summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRobey Pointer <robey@lag.net>2003-11-04 08:34:24 +0000
committerRobey Pointer <robey@lag.net>2003-11-04 08:34:24 +0000
commit51607386c7609a483568ad935083c9668fe6241b (patch)
tree46b1083cfbd387fd181cc8fbef2ce77f837a3bd6
downloadparamiko-51607386c7609a483568ad935083c9668fe6241b.tar.gz
[project @ Arch-1:robey@lag.net--2003-public%secsh--dev--1.0--base-0]
initial import (automatically generated log message)
-rw-r--r--LICENSE504
-rw-r--r--MANIFEST13
-rw-r--r--Makefile22
-rw-r--r--NOTES13
-rw-r--r--README166
-rw-r--r--auth_transport.py224
-rw-r--r--ber.py112
-rw-r--r--channel.py608
-rwxr-xr-xdemo-server.py56
-rwxr-xr-xdemo.py180
-rw-r--r--dsskey.py121
-rw-r--r--kex_gex.py180
-rw-r--r--kex_group1.py104
-rw-r--r--message.py119
-rw-r--r--rsakey.py102
-rw-r--r--secsh.py23
-rw-r--r--setup.py30
-rw-r--r--transport.py758
-rw-r--r--util.py89
19 files changed, 3424 insertions, 0 deletions
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..b1e3f5a2
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,504 @@
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL. It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+ This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it. You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+ When we speak of free software, we are referring to freedom of use,
+not price. Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+ To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights. These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+ For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you. You must make sure that they, too, receive or can get the source
+code. If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it. And you must show them these terms so they know their rights.
+
+ We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+ To protect each distributor, we want to make it very clear that
+there is no warranty for the free library. Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+ Finally, software patents pose a constant threat to the existence of
+any free program. We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder. Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+ Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License. This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License. We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+ When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library. The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom. The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+ We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License. It also provides other free software developers Less
+of an advantage over competing non-free programs. These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries. However, the Lesser license provides advantages in certain
+special circumstances.
+
+ For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard. To achieve this, non-free programs must be
+allowed to use the library. A more frequent case is that a free
+library does the same job as widely used non-free libraries. In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+ In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software. For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+ Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+ The precise terms and conditions for copying, distribution and
+modification follow. Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library". The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+ A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+ The "Library", below, refers to any such software library or work
+which has been distributed under these terms. A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language. (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+ "Source code" for a work means the preferred form of the work for
+making modifications to it. For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+ Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it). Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+ 1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+ You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+ 2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) The modified work must itself be a software library.
+
+ b) You must cause the files modified to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ c) You must cause the whole of the work to be licensed at no
+ charge to all third parties under the terms of this License.
+
+ d) If a facility in the modified Library refers to a function or a
+ table of data to be supplied by an application program that uses
+ the facility, other than as an argument passed when the facility
+ is invoked, then you must make a good faith effort to ensure that,
+ in the event an application does not supply such function or
+ table, the facility still operates, and performs whatever part of
+ its purpose remains meaningful.
+
+ (For example, a function in a library to compute square roots has
+ a purpose that is entirely well-defined independent of the
+ application. Therefore, Subsection 2d requires that any
+ application-supplied function or table used by this function must
+ be optional: if the application does not supply it, the square
+ root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library. To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License. (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.) Do not make any other change in
+these notices.
+
+ Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+ This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+ 4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+ If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library". Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+ However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library". The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+ When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library. The
+threshold for this to be true is not precisely defined by law.
+
+ If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work. (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+ Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+ 6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+ You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License. You must supply a copy of this License. If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License. Also, you must do one
+of these things:
+
+ a) Accompany the work with the complete corresponding
+ machine-readable source code for the Library including whatever
+ changes were used in the work (which must be distributed under
+ Sections 1 and 2 above); and, if the work is an executable linked
+ with the Library, with the complete machine-readable "work that
+ uses the Library", as object code and/or source code, so that the
+ user can modify the Library and then relink to produce a modified
+ executable containing the modified Library. (It is understood
+ that the user who changes the contents of definitions files in the
+ Library will not necessarily be able to recompile the application
+ to use the modified definitions.)
+
+ b) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (1) uses at run time a
+ copy of the library already present on the user's computer system,
+ rather than copying library functions into the executable, and (2)
+ will operate properly with a modified version of the library, if
+ the user installs one, as long as the modified version is
+ interface-compatible with the version that the work was made with.
+
+ c) Accompany the work with a written offer, valid for at
+ least three years, to give the same user the materials
+ specified in Subsection 6a, above, for a charge no more
+ than the cost of performing this distribution.
+
+ d) If distribution of the work is made by offering access to copy
+ from a designated place, offer equivalent access to copy the above
+ specified materials from the same place.
+
+ e) Verify that the user has already received a copy of these
+ materials or that you have already sent this user a copy.
+
+ For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it. However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+ It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system. Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+ 7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+ a) Accompany the combined library with a copy of the same work
+ based on the Library, uncombined with any other library
+ facilities. This must be distributed under the terms of the
+ Sections above.
+
+ b) Give prominent notice with the combined library of the fact
+ that part of it is a work based on the Library, and explaining
+ where to find the accompanying uncombined form of the same work.
+
+ 8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License. Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License. However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+ 9. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Library or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+ 10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+ 11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all. For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded. In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+ 13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation. If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+ 14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission. For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this. Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+ NO WARRANTY
+
+ 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Libraries
+
+ If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change. You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+ To apply these terms, attach the following notices to the library. It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the library's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This library 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 library 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 library; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the
+ library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+ <signature of Ty Coon>, 1 April 1990
+ Ty Coon, President of Vice
+
+That's all there is to it!
+
+
diff --git a/MANIFEST b/MANIFEST
new file mode 100644
index 00000000..68d7bdee
--- /dev/null
+++ b/MANIFEST
@@ -0,0 +1,13 @@
+README
+ber.py
+channel.py
+dsskey.py
+kex_gex.py
+kex_group1.py
+message.py
+rsakey.py
+secsh.py
+setup.py
+transport.py
+util.py
+demo.py
diff --git a/Makefile b/Makefile
new file mode 100644
index 00000000..beb5526b
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,22 @@
+# releases:
+# aerodactyl (13sep03)
+# bulbasaur
+# charmander
+
+RELEASE=bulbasaur
+
+release:
+ mkdir ../secsh-$(RELEASE)
+ cp README ../secsh-$(RELEASE)
+ cp *.py ../secsh-$(RELEASE)
+ cd .. && zip -r secsh-$(RELEASE).zip secsh-$(RELEASE)
+ echo rm -rf ../secsh-$(RELEASE)
+
+py:
+ python ./setup.py sdist --formats=zip
+
+# places where the version number is stored:
+#
+# setup.py
+# secsh.py
+# README
diff --git a/NOTES b/NOTES
new file mode 100644
index 00000000..9e8ce06e
--- /dev/null
+++ b/NOTES
@@ -0,0 +1,13 @@
+
+ +-------------------+ +-----------------+
+(Socket)InputStream ---> | secsh transport | <===> | secsh channel |
+(Socket)OutputStream --> | (auth, pipe) | N | (buffer) |
+ +-------------------+ +-----------------+
+ @ feeder thread | |
+ - read InputStream | +-> InputStream
+ - feed into channel +---> OutputStream
+ buffers
+
+SIS <-- @ --> (parse, find chan) --> secsh chan: buffer <-- SSHInputStream
+SSHOutputStream --> secsh chan --> secsh transport --> SOS [no thread]
+
diff --git a/README b/README
new file mode 100644
index 00000000..d861ff26
--- /dev/null
+++ b/README
@@ -0,0 +1,166 @@
+secsh 0.1
+"bulbasaur" release, 18 sep 2003
+
+(c) 2003 Robey Pointer <robey@lag.net>
+
+http://www.lag.net/~robey/secsh/
+
+
+*** WHAT
+
+secsh is a module for python 2.3 that implements the SSH2 protocol for secure
+(encrypted and authenticated) connections to remote machines. unlike SSL (aka
+TLS), SSH2 protocol does not require heirarchical certificates signed by a
+powerful central authority. you may know SSH2 as the protocol that replaced
+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 secsh module.
+
+it is written entirely in python (no C or platform-dependent code) and is
+released under the GNU LGPL (lesser GPL).
+
+
+*** REQUIREMENTS
+
+python 2.3 <http://www.python.org/>
+pyCrypto <http://www.amk.ca/python/code/crypto.html>
+
+
+*** PORTABILITY
+
+i code and test this library on Linux and MacOS X. for that reason, i'm
+pretty sure that it works for all posix platforms, including MacOS. i also
+think it will work on Windows, though i've never tested it there. if you
+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.])
+
+
+*** DEMO
+
+the demo app (demo.py) is a raw implementation of the normal 'ssh' CLI tool.
+while the secsh library should work on all platforms, the demo app will only
+run on posix, because it uses select.
+
+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.
+
+the demo app leaves a logfile called "demo.log" so you can see what secsh
+logs as it works. but the most interesting part is probably the code itself,
+which hopefully demonstrates how you can use the secsh library.
+
+
+*** 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).
+
+
+*** CHANGELOG
+
+2003-08-24:
+ * implemented the other hashes: all 4 from the draft are working now
+ * added 'aes128-cbc' and '3des-cbc' cipher support
+ * fixed channel eof/close semantics
+2003-09-12: version "aerodactyl"
+ * implemented group-exchange kex ("kex-gex")
+ * implemented RSA/DSA private key auth
+2003-09-13:
+ * fixed inflate_long and deflate_long to handle negatives, even though
+ they're never used in the current ssh protocol
+2003-09-14:
+ * fixed session_id handling: re-keying works now
+ * added the ability for a Channel to have a fileno() for select/poll
+ purposes, although this will cause worse window performance if the
+ client app isn't careful
+2003-09-16: version "bulbasaur"
+ * fixed pipe (fileno) method to be nonblocking and it seems to work now
+ * fixed silly bug that caused large blocks to be truncated
+2003-10-08:
+ * patch to fix Channel.invoke_subsystem and add Channel.exec_command
+ [vaclav dvorak]
+ * patch to add Channel.sendall [vaclav dvorak]
+ * patch to add Channel.shutdown [vaclav dvorak]
+ * patch to add Channel.makefile and a ChannelFile class which emulates
+ a python file object [vaclav dvorak]
+2003-10-26:
+ * thread creation no longer happens during construction -- use the new
+ method "start_client(event)" to get things rolling
+ * re-keying now takes place after 1GB of data or 1 billion packets
+ (these limits can be easily changed per-session if needed)
+
+
+*** 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)
+* should have a simple synchronous method that handles all auth & events,
+ by pre-seeding the password or key info, and the expected key
diff --git a/auth_transport.py b/auth_transport.py
new file mode 100644
index 00000000..1a06326d
--- /dev/null
+++ b/auth_transport.py
@@ -0,0 +1,224 @@
+#!/usr/bin/python
+
+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
+from message import Message
+from secsh import SSHException
+from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL
+
+DISCONNECT_SERVICE_NOT_AVAILABLE, DISCONNECT_AUTH_CANCELLED_BY_USER, \
+ DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE = 7, 13, 14
+
+AUTH_SUCCESSFUL, AUTH_PARTIALLY_SUCCESSFUL, AUTH_FAILED = range(3)
+
+
+class Transport(BaseTransport):
+ "BaseTransport with the auth framework hooked up"
+ def __init__(self, sock):
+ BaseTransport.__init__(self, sock)
+ self.auth_event = None
+ # for server mode:
+ self.auth_username = None
+ self.auth_fail_count = 0
+ self.auth_complete = 0
+
+ def request_auth(self):
+ m = Message()
+ m.add_byte(chr(MSG_SERVICE_REQUEST))
+ m.add_string('ssh-userauth')
+ self.send_message(m)
+
+ def auth_key(self, username, key, event):
+ if (not self.active) or (not self.initial_kex_done):
+ # we should never try to send the password unless we're on a secure link
+ raise SSHException('No existing session')
+ try:
+ self.lock.acquire()
+ self.auth_event = event
+ self.auth_method = 'publickey'
+ self.username = username
+ self.private_key = key
+ self.request_auth()
+ finally:
+ self.lock.release()
+
+ def auth_password(self, username, password, event):
+ 'authenticate using a password; event is triggered on success or fail'
+ if (not self.active) or (not self.initial_kex_done):
+ # we should never try to send the password unless we're on a secure link
+ raise SSHException('No existing session')
+ try:
+ self.lock.acquire()
+ self.auth_event = event
+ self.auth_method = 'password'
+ self.username = username
+ self.password = password
+ self.request_auth()
+ finally:
+ self.lock.release()
+
+ def disconnect_service_not_available(self):
+ m = Message()
+ m.add_byte(chr(MSG_DISCONNECT))
+ m.add_int(DISCONNECT_SERVICE_NOT_AVAILABLE)
+ m.add_string('Service not available')
+ m.add_string('en')
+ self.send_message(m)
+ self.close()
+
+ def disconnect_no_more_auth(self):
+ m = Message()
+ m.add_byte(chr(MSG_DISCONNECT))
+ m.add_int(DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE)
+ m.add_string('No more auth methods available')
+ m.add_string('en')
+ self.send_message(m)
+ self.close()
+
+ def parse_service_request(self, m):
+ service = m.get_string()
+ if self.server_mode and (service == 'ssh-userauth'):
+ # accepted
+ m = Message()
+ m.add_byte(chr(MSG_SERVICE_ACCEPT))
+ m.add_string(service)
+ self.send_message(m)
+ return
+ # dunno this one
+ self.disconnect_service_not_available()
+
+ def parse_service_accept(self, m):
+ service = m.get_string()
+ if service == 'ssh-userauth':
+ self.log(DEBUG, 'userauth is OK')
+ m = Message()
+ m.add_byte(chr(MSG_USERAUTH_REQUEST))
+ m.add_string(self.username)
+ m.add_string('ssh-connection')
+ m.add_string(self.auth_method)
+ if self.auth_method == 'password':
+ m.add_boolean(0)
+ m.add_string(self.password.encode('UTF-8'))
+ elif self.auth_method == 'publickey':
+ m.add_boolean(1)
+ m.add_string(self.private_key.get_name())
+ m.add_string(str(self.private_key))
+ m.add_string(self.private_key.sign_ssh_session(self.randpool, self.H, self.username))
+ else:
+ raise SSHException('Unknown auth method "%s"' % self.auth_method)
+ self.send_message(m)
+ else:
+ self.log(DEBUG, 'Service request "%s" accepted (?)' % service)
+
+ def get_allowed_auths(self):
+ "override me!"
+ return 'password'
+
+ def check_auth_none(self, username):
+ "override me! return tuple of (int, string) ==> (auth status, list of acceptable auth methods)"
+ return (AUTH_FAILED, self.get_allowed_auths())
+
+ def check_auth_password(self, username, password):
+ "override me! return tuple of (int, string) ==> (auth status, list of acceptable auth methods)"
+ return (AUTH_FAILED, self.get_allowed_auths())
+
+ def check_auth_publickey(self, username, key):
+ "override me! return tuple of (int, string) ==> (auth status, list of acceptable auth methods)"
+ return (AUTH_FAILED, self.get_allowed_auths())
+
+ def parse_userauth_request(self, m):
+ if not self.server_mode:
+ # er, uh... what?
+ m = Message()
+ m.add_byte(chr(MSG_USERAUTH_FAILURE))
+ m.add_string('none')
+ m.add_boolean(0)
+ self.send_message(m)
+ return
+ if self.auth_complete:
+ # ignore
+ return
+ username = m.get_string()
+ service = m.get_string()
+ method = m.get_string()
+ if service != 'ssh-connection':
+ self.disconnect_service_not_available()
+ return
+ if (self.auth_username is not None) and (self.auth_username != username):
+ # trying to change username in mid-flight!
+ self.disconnect_no_more_auth()
+ return
+ if method == 'none':
+ result = self.check_auth_none(username)
+ elif method == 'password':
+ changereq = m.get_boolean()
+ password = m.get_string().decode('UTF-8')
+ if changereq:
+ # always treated as failure, since we don't support changing passwords, but collect
+ # the list of valid auth types from the callback anyway
+ newpassword = m.get_string().decode('UTF-8')
+ result = self.check_auth_password(username, password)
+ result = (AUTH_FAILED, result[1])
+ else:
+ result = self.check_auth_password(username, password)
+ elif method == 'publickey':
+ # FIXME
+ result = self.check_auth_none(username)
+ result = (AUTH_FAILED, result[1])
+ else:
+ result = self.check_auth_none(username)
+ result = (AUTH_FAILED, result[1])
+ # okay, send result
+ m = Message()
+ if result[0] == AUTH_SUCCESSFUL:
+ m.add_byte(chr(MSG_USERAUTH_SUCCESSFUL))
+ self.auth_complete = 1
+ else:
+ m.add_byte(chr(MSG_USERAUTH_FAILURE))
+ m.add_string(result[1])
+ if result[0] == AUTH_PARTIALLY_SUCCESSFUL:
+ m.add_boolean(1)
+ else:
+ m.add_boolean(0)
+ self.auth_fail_count += 1
+ self.send_message(m)
+ if self.auth_fail_count >= 10:
+ self.disconnect_no_more_auth()
+
+ def parse_userauth_success(self, m):
+ self.log(INFO, 'Authentication successful!')
+ self.authenticated = 1
+ if self.auth_event != None:
+ self.auth_event.set()
+
+ def parse_userauth_failure(self, m):
+ authlist = m.get_list()
+ partial = m.get_boolean()
+ if partial:
+ self.log(INFO, 'Authentication continues...')
+ self.log(DEBUG, 'Methods: ' + str(partial))
+ # FIXME - do something
+ pass
+ self.log(INFO, 'Authentication failed.')
+ self.authenticated = 0
+ self.close()
+ if self.auth_event != None:
+ self.auth_event.set()
+
+ def parse_userauth_banner(self, m):
+ banner = m.get_string()
+ lang = m.get_string()
+ self.log(INFO, 'Auth banner: ' + banner)
+ # who cares.
+
+ handler_table = BaseTransport.handler_table.copy()
+ handler_table.update({
+ MSG_SERVICE_REQUEST: parse_service_request,
+ MSG_SERVICE_ACCEPT: parse_service_accept,
+ MSG_USERAUTH_REQUEST: parse_userauth_request,
+ MSG_USERAUTH_SUCCESS: parse_userauth_success,
+ MSG_USERAUTH_FAILURE: parse_userauth_failure,
+ MSG_USERAUTH_BANNER: parse_userauth_banner,
+ })
+
diff --git a/ber.py b/ber.py
new file mode 100644
index 00000000..7fe1dd09
--- /dev/null
+++ b/ber.py
@@ -0,0 +1,112 @@
+#!/usr/bin/python
+
+import struct
+
+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 BER(object):
+
+ def __init__(self, content=''):
+ self.content = content
+ self.idx = 0
+
+ def __str__(self):
+ return self.content
+
+ def __repr__(self):
+ return 'BER(' + repr(self.content) + ')'
+
+ def decode(self):
+ return self.decode_next()
+
+ def decode_next(self):
+ if self.idx >= len(self.content):
+ return None
+ id = ord(self.content[self.idx])
+ self.idx += 1
+ if (id & 31) == 31:
+ # identifier > 30
+ id = 0
+ while self.idx < len(self.content):
+ t = ord(self.content[self.idx])
+ if not (t & 0x80):
+ break
+ id = (id << 7) | (t & 0x7f)
+ self.idx += 1
+ if self.idx >= len(self.content):
+ return None
+ # now fetch length
+ size = ord(self.content[self.idx])
+ self.idx += 1
+ if size & 0x80:
+ # more complimicated...
+ # FIXME: theoretically should handle indefinite-length (0x80)
+ t = size & 0x7f
+ if self.idx + t > len(self.content):
+ return None
+ size = 0
+ while t > 0:
+ size = (size << 8) | ord(self.content[self.idx])
+ self.idx += 1
+ t -= 1
+ if self.idx + size > len(self.content):
+ # can't fit
+ return None
+ data = self.content[self.idx : self.idx + size]
+ self.idx += size
+ # now switch on id
+ if id == 0x30:
+ # sequence
+ return self.decode_sequence(data)
+ elif id == 2:
+ # int
+ return inflate_long(data)
+ else:
+ # 1: boolean (00 false, otherwise true)
+ raise Exception('Unknown ber encoding type %d (robey is lazy)' % id)
+
+ def decode_sequence(data):
+ out = []
+ b = BER(data)
+ while 1:
+ x = b.decode_next()
+ if x == None:
+ return out
+ out.append(x)
+ decode_sequence = staticmethod(decode_sequence)
+
diff --git a/channel.py b/channel.py
new file mode 100644
index 00000000..275c0a26
--- /dev/null
+++ b/channel.py
@@ -0,0 +1,608 @@
+from message import Message
+from secsh import SSHException
+from transport import MSG_CHANNEL_REQUEST, MSG_CHANNEL_CLOSE, MSG_CHANNEL_WINDOW_ADJUST, MSG_CHANNEL_DATA, \
+ MSG_CHANNEL_EOF
+
+import time, threading, logging, socket, os
+from logging import DEBUG
+
+
+# this is ugly, and won't work on windows
+def set_nonblocking(fd):
+ import fcntl
+ fcntl.fcntl(fd, fcntl.F_SETFL, os.O_NONBLOCK)
+
+
+class Channel(object):
+ """
+ Abstraction for a secsh channel.
+ """
+
+ def __init__(self, chanid, transport):
+ self.chanid = chanid
+ self.transport = transport
+ self.active = 0
+ self.eof_received = 0
+ self.eof_sent = 0
+ self.in_buffer = ''
+ self.timeout = None
+ self.closed = 0
+ self.lock = threading.Lock()
+ self.in_buffer_cv = threading.Condition(self.lock)
+ self.out_buffer_cv = threading.Condition(self.lock)
+ self.name = str(chanid)
+ self.logger = logging.getLogger('secsh.chan.' + str(chanid))
+ self.pipe_rfd = self.pipe_wfd = None
+
+ def __repr__(self):
+ out = '<secsh.Channel %d' % self.chanid
+ if self.closed:
+ out += ' (closed)'
+ elif self.active:
+ if self.eof_received:
+ out += ' (EOF received)'
+ if self.eof_sent:
+ out += ' (EOF sent)'
+ out += ' (open) window=%d' % (self.out_window_size)
+ if len(self.in_buffer) > 0:
+ out += ' in-buffer=%d' % (len(self.in_buffer),)
+ out += ' -> ' + repr(self.transport)
+ out += '>'
+ return out
+
+ def log(self, level, msg):
+ self.logger.log(level, msg)
+
+ def set_window(self, window_size, max_packet_size):
+ self.in_window_size = window_size
+ self.in_max_packet_size = max_packet_size
+ # threshold of bytes we receive before we bother to send a window update
+ self.in_window_threshold = window_size // 10
+ self.in_window_sofar = 0
+
+ def set_server_channel(self, chanid, window_size, max_packet_size):
+ self.server_chanid = chanid
+ self.out_window_size = window_size
+ self.out_max_packet_size = max_packet_size
+ self.active = 1
+
+ def request_success(self, m):
+ self.log(DEBUG, 'Sesch channel %d request ok' % self.chanid)
+ return
+
+ def request_failed(self, m):
+ self.close()
+
+ def feed(self, m):
+ s = m.get_string()
+ try:
+ self.lock.acquire()
+ self.log(DEBUG, 'fed %d bytes' % len(s))
+ if self.pipe_wfd != None:
+ self.feed_pipe(s)
+ else:
+ self.in_buffer += s
+ self.in_buffer_cv.notifyAll()
+ self.log(DEBUG, '(out from feed)')
+ finally:
+ self.lock.release()
+
+ def window_adjust(self, m):
+ nbytes = m.get_int()
+ try:
+ self.lock.acquire()
+ self.log(DEBUG, 'window up %d' % nbytes)
+ self.out_window_size += nbytes
+ self.out_buffer_cv.notifyAll()
+ finally:
+ self.lock.release()
+
+ def handle_request(self, m):
+ key = m.get_string()
+ if key == 'exit-status':
+ self.exit_status = m.get_int()
+ return
+ elif key == 'xon-xoff':
+ # ignore
+ return
+ else:
+ self.log(DEBUG, 'Unhandled channel request "%s"' % key)
+
+ def handle_eof(self, m):
+ self.eof_received = 1
+ try:
+ self.lock.acquire()
+ self.in_buffer_cv.notifyAll()
+ if self.pipe_wfd != None:
+ os.close(self.pipe_wfd)
+ self.pipe_wfd = None
+ finally:
+ self.lock.release()
+ self.log(DEBUG, 'EOF received')
+
+ def handle_close(self, m):
+ self.close()
+ try:
+ self.lock.acquire()
+ self.in_buffer_cv.notifyAll()
+ self.out_buffer_cv.notifyAll()
+ if self.pipe_wfd != None:
+ os.close(self.pipe_wfd)
+ self.pipe_wfd = None
+ finally:
+ self.lock.release()
+
+
+ # API for external use
+
+ def get_pty(self, term='vt100', width=80, height=24):
+ if self.closed or self.eof_received or self.eof_sent or not self.active:
+ raise SSHException('Channel is not open')
+ m = Message()
+ m.add_byte(chr(MSG_CHANNEL_REQUEST))
+ m.add_int(self.server_chanid)
+ m.add_string('pty-req')
+ m.add_boolean(0)
+ m.add_string(term)
+ m.add_int(width)
+ m.add_int(height)
+ # pixel height, width (usually useless)
+ m.add_int(0).add_int(0)
+ m.add_string('')
+ self.transport.send_message(m)
+
+ def invoke_shell(self):
+ if self.closed or self.eof_received or self.eof_sent or not self.active:
+ raise SSHException('Channel is not open')
+ m = Message()
+ m.add_byte(chr(MSG_CHANNEL_REQUEST))
+ m.add_int(self.server_chanid)
+ m.add_string('shell')
+ m.add_boolean(1)
+ self.transport.send_message(m)
+
+ def exec_command(self, command):
+ if self.closed or self.eof_received or self.eof_sent or not self.active:
+ raise SSHException('Channel is not open')
+ m = Message()
+ m.add_byte(chr(MSG_CHANNEL_REQUEST))
+ m.add_int(self.server_chanid)
+ m.add_string('exec')
+ m.add_boolean(1)
+ m.add_string(command)
+ self.transport.send_message(m)
+
+ def invoke_subsystem(self, subsystem):
+ if self.closed or self.eof_received or self.eof_sent or not self.active:
+ raise SSHException('Channel is not open')
+ m = Message()
+ m.add_byte(chr(MSG_CHANNEL_REQUEST))
+ m.add_int(self.server_chanid)
+ m.add_string('subsystem')
+ m.add_boolean(1)
+ m.add_string(subsystem)
+ self.transport.send_message(m)
+
+ def resize_pty(self, width=80, height=24):
+ if self.closed or self.eof_received or self.eof_sent or not self.active:
+ raise SSHException('Channel is not open')
+ m = Message()
+ m.add_byte(chr(MSG_CHANNEL_REQUEST))
+ m.add_int(self.server_chanid)
+ m.add_string('window-change')
+ m.add_boolean(0)
+ m.add_int(width)
+ m.add_int(height)
+ m.add_int(0).add_int(0)
+ self.transport.send_message(m)
+
+ def get_transport(self):
+ return self.transport
+
+ def set_name(self, name):
+ self.name = name
+ self.logger = logging.getLogger('secsh.chan.' + name)
+
+ def get_name(self):
+ return self.name
+
+ def send_eof(self):
+ if self.eof_sent:
+ return
+ m = Message()
+ m.add_byte(chr(MSG_CHANNEL_EOF))
+ m.add_int(self.server_chanid)
+ self.transport.send_message(m)
+ self.eof_sent = 1
+ self.log(DEBUG, 'EOF sent')
+ return
+
+
+ # socket equivalency methods...
+
+ def settimeout(self, timeout):
+ self.timeout = timeout
+
+ def gettimeout(self):
+ return self.timeout
+
+ def setblocking(self, blocking):
+ if blocking:
+ self.settimeout(None)
+ else:
+ self.settimeout(0.0)
+
+ def close(self):
+ if self.closed or not self.active:
+ return
+ self.send_eof()
+ m = Message()
+ m.add_byte(chr(MSG_CHANNEL_CLOSE))
+ m.add_int(self.server_chanid)
+ self.transport.send_message(m)
+ self.closed = 1
+ self.transport.unlink_channel(self.chanid)
+
+ def recv_ready(self):
+ "doesn't work if you've called fileno()"
+ try:
+ self.lock.acquire()
+ if len(self.in_buffer) == 0:
+ return 0
+ return 1
+ finally:
+ self.lock.release()
+
+ def recv(self, nbytes):
+ out = ''
+ try:
+ self.lock.acquire()
+ if self.pipe_rfd != None:
+ # use the pipe
+ return self.read_pipe(nbytes)
+ if len(self.in_buffer) == 0:
+ if self.closed or self.eof_received:
+ return out
+ # should we block?
+ if self.timeout == 0.0:
+ raise socket.timeout()
+ # loop here in case we get woken up but a different thread has grabbed everything in the buffer
+ timeout = self.timeout
+ while (len(self.in_buffer) == 0) and not self.closed and not self.eof_received:
+ then = time.time()
+ self.in_buffer_cv.wait(timeout)
+ if timeout != None:
+ timeout -= time.time() - then
+ if timeout <= 0.0:
+ raise socket.timeout()
+ # something in the buffer and we have the lock
+ if len(self.in_buffer) <= nbytes:
+ out = self.in_buffer
+ self.in_buffer = ''
+ else:
+ out = self.in_buffer[:nbytes]
+ self.in_buffer = self.in_buffer[nbytes:]
+ self.check_add_window(len(out))
+ finally:
+ self.lock.release()
+ return out
+
+ def send(self, s):
+ size = 0
+ if self.closed or self.eof_sent:
+ return size
+ try:
+ self.lock.acquire()
+ if self.out_window_size == 0:
+ # should we block?
+ if self.timeout == 0.0:
+ raise socket.timeout()
+ # loop here in case we get woken up but a different thread has filled the buffer
+ timeout = self.timeout
+ while self.out_window_size == 0:
+ then = time.time()
+ self.out_buffer_cv.wait(timeout)
+ if timeout != None:
+ timeout -= time.time() - then
+ if timeout <= 0.0:
+ raise socket.timeout()
+ # we have some window to squeeze into
+ if self.closed:
+ return 0
+ size = len(s)
+ if self.out_window_size < size:
+ size = self.out_window_size
+ if self.out_max_packet_size < size:
+ size = self.out_max_packet_size
+ m = Message()
+ m.add_byte(chr(MSG_CHANNEL_DATA))
+ m.add_int(self.server_chanid)
+ m.add_string(s[:size])
+ self.transport.send_message(m)
+ self.out_window_size -= size
+ finally:
+ self.lock.release()
+ return size
+
+ def sendall(self, s):
+ while s:
+ if self.closed:
+ # this doesn't seem useful, but it is the documented behavior of Socket
+ raise socket.error('Socket is closed')
+ sent = self.send(s)
+ s = s[sent:]
+ return None
+
+ def makefile(self, *params):
+ return ChannelFile(*([self] + list(params)))
+
+ def fileno(self):
+ """
+ returns an OS-level fd which can be used for polling and reading (but
+ NOT for writing). this is primarily to allow python's \"select\" module
+ to work. the first time this function is called, a pipe is created to
+ simulate real OS-level fd behavior. because of this, two actual fds are
+ created: one to return and one to feed. this may be inefficient if you
+ plan to use many fds.
+
+ the channel's receive window will be updated as data comes in, not as
+ you read it, so if you fail to poll the channel often enough, it may
+ block ALL channels across the transport.
+ """
+ try:
+ self.lock.acquire()
+ if self.pipe_rfd != None:
+ return self.pipe_rfd
+ # create the pipe and feed in any existing data
+ self.pipe_rfd, self.pipe_wfd = os.pipe()
+ set_nonblocking(self.pipe_wfd)
+ set_nonblocking(self.pipe_rfd)
+ if len(self.in_buffer) > 0:
+ x = self.in_buffer
+ self.in_buffer = ''
+ self.feed_pipe(x)
+ return self.pipe_rfd
+ finally:
+ self.lock.release()
+
+ def shutdown(self, how):
+ if (how == 0) or (how == 2):
+ # feign "read" shutdown
+ self.eof_received = 1
+ if (how == 1) or (how == 2):
+ self.send_eof()
+
+
+ # internal use...
+
+ def feed_pipe(self, data):
+ "you are already holding the lock"
+ if len(self.in_buffer) > 0:
+ self.in_buffer += data
+ return
+ try:
+ n = os.write(self.pipe_wfd, data)
+ if n < len(data):
+ # at least on linux, this will never happen, as the writes are
+ # considered atomic... but just in case.
+ self.in_buffer = data[n:]
+ self.check_add_window(n)
+ self.in_buffer_cv.notifyAll()
+ return
+ except OSError, e:
+ pass
+ if len(data) > 1:
+ # try writing just one byte then
+ x = data[0]
+ data = data[1:]
+ try:
+ os.write(self.pipe_wfd, x)
+ self.in_buffer = data
+ self.check_add_window(1)
+ self.in_buffer_cv.notifyAll()
+ return
+ except OSError, e:
+ data = x + data
+ # pipe is very full
+ self.in_buffer = data
+ self.in_buffer_cv.notifyAll()
+
+ def read_pipe(self, nbytes):
+ "you are already holding the lock"
+ try:
+ x = os.read(self.pipe_rfd, nbytes)
+ if len(x) > 0:
+ self.push_pipe(len(x))
+ return x
+ except OSError, e:
+ pass
+ # nothing in the pipe
+ if self.closed or self.eof_received:
+ return ''
+ # should we block?
+ if self.timeout == 0.0:
+ raise socket.timeout()
+ # loop here in case we get woken up but a different thread has grabbed everything in the buffer
+ timeout = self.timeout
+ while not self.closed and not self.eof_received:
+ then = time.time()
+ self.in_buffer_cv.wait(timeout)
+ if timeout != None:
+ timeout -= time.time() - then
+ if timeout <= 0.0:
+ raise socket.timeout()
+ try:
+ x = os.read(self.pipe_rfd, nbytes)
+ if len(x) > 0:
+ self.push_pipe(len(x))
+ return x
+ except OSError, e:
+ pass
+ pass
+
+ def push_pipe(self, nbytes):
+ # successfully read N bytes from the pipe, now re-feed the pipe if necessary
+ # (assumption: the pipe can hold as many bytes as were read out)
+ if len(self.in_buffer) == 0:
+ return
+ if len(self.in_buffer) <= nbytes:
+ os.write(self.pipe_wfd, self.in_buffer)
+ self.in_buffer = ''
+ return
+ x = self.in_buffer[:nbytes]
+ self.in_buffer = self.in_buffer[nbytes:]
+ os.write(self.pipd_wfd, x)
+
+ def unlink(self):
+ if self.closed or not self.active:
+ return
+ self.closed = 1
+ self.transport.unlink_channel(self.chanid)
+
+ def check_add_window(self, n):
+ # already holding the lock!
+ if self.closed or self.eof_received or not self.active:
+ return
+ self.log(DEBUG, 'addwindow %d' % n)
+ self.in_window_sofar += n
+ if self.in_window_sofar > self.in_window_threshold:
+ self.log(DEBUG, 'addwindow send %d' % self.in_window_sofar)
+ m = Message()
+ m.add_byte(chr(MSG_CHANNEL_WINDOW_ADJUST))
+ m.add_int(self.server_chanid)
+ m.add_int(self.in_window_sofar)
+ self.transport.send_message(m)
+ self.in_window_sofar = 0
+
+
+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.
+ XXX Todo: translation of the various forms of newline is not implemented,
+ let alone the universal newline. Line buffering (for writing) is
+ implemented, though, which makes little sense without text mode support.
+ """
+
+ def __init__(self, channel, mode = "r", buf_size = -1):
+ self.channel = channel
+ self.mode = mode
+ if buf_size < 0:
+ self.buf_size = 1024
+ self.line_buffered = 0
+ elif buf_size == 1:
+ self.buf_size = 1
+ self.line_buffered = 1
+ else:
+ self.buf_size = buf_size
+ self.line_buffered = 0
+ self.wbuffer = ""
+ self.rbuffer = ""
+ self.readable = ("r" in mode)
+ self.writable = ("w" in mode) or ("+" in mode) or ("a" in mode)
+ self.binary = ("b" in mode)
+ if not self.binary:
+ raise NotImplementedError("text mode not supported")
+ self.softspace = 0
+
+ def __iter__(self):
+ return self
+
+ def next(self):
+ line = self.readline()
+ if not line:
+ raise StopIteration
+ return line
+
+ def write(self, str):
+ if not self.writable:
+ raise IOError("file not open for writing")
+ if self.buf_size == 0 and not self.line_buffered:
+ self.channel.sendall(str)
+ return
+ self.wbuffer += str
+ if self.line_buffered:
+ last_newline_pos = self.wbuffer.rfind("\n")
+ if last_newline_pos >= 0:
+ self.channel.sendall(self.wbuffer[:last_newline_pos+1])
+ self.wbuffer = self.wbuffer[last_newline_pos+1:]
+ else:
+ if len(self.wbuffer) >= self.buf_size:
+ self.channel.sendall(self.wbuffer)
+ self.wbuffer = ""
+ return
+
+ def writelines(self, sequence):
+ for s in sequence:
+ self.write(s)
+ return
+
+ def flush(self):
+ self.channel.sendall(self.wbuffer)
+ self.wbuffer = ""
+ return
+
+ def read(self, size = None):
+ if not self.readable:
+ raise IOError("file not open for reading")
+ if size is None or size < 0:
+ result = self.rbuffer
+ self.rbuffer = ""
+ while not self.channel.eof_received:
+ new_data = self.channel.recv(65536)
+ if not new_data:
+ break
+ result += new_data
+ return result
+ if size <= len(self.rbuffer):
+ result = self.rbuffer[:size]
+ self.rbuffer = self.rbuffer[size:]
+ return result
+ while len(self.rbuffer) < size and not self.channel.eof_received:
+ new_data = self.channel.recv(max(self.buf_size, size-len(self.rbuffer)))
+ if not new_data:
+ break
+ self.rbuffer += new_data
+ result = self.rbuffer[:size]
+ self.rbuffer[size:]
+ return result
+
+ def readline(self, size = None):
+ line = ""
+ while "\n" not in line:
+ if size >= 0:
+ new_data = self.read(size - len(line))
+ else:
+ new_data = self.read(64)
+ if not new_data:
+ break
+ line += new_data
+ newline_pos = line.find("\n")
+ if newline_pos >= 0:
+ self.rbuffer = line[newline_pos+1:] + self.rbuffer
+ return line[:newline_pos+1]
+ elif len(line) > size:
+ self.rbuffer = line[size:] + self.rbuffer
+ return line[:size]
+ return line
+
+ def readlines(self, sizehint = None):
+ lines = []
+ while 1:
+ line = self.readline()
+ if not line:
+ break
+ lines.append(line)
+ return lines
+
+ def xreadlines(self):
+ return self
+
+ def close(self):
+ self.flush()
+ return
+
+# vim: set shiftwidth=4 expandtab :
diff --git a/demo-server.py b/demo-server.py
new file mode 100755
index 00000000..6819ea92
--- /dev/null
+++ b/demo-server.py
@@ -0,0 +1,56 @@
+#!/usr/bin/python
+
+import sys, os, socket, threading, logging, traceback
+import secsh
+
+# setup logging
+l = logging.getLogger("secsh")
+l.setLevel(logging.DEBUG)
+if len(l.handlers) == 0:
+ f = open('demo-server.log', 'w')
+ lh = logging.StreamHandler(f)
+ lh.setFormatter(logging.Formatter('%(levelname)-.3s [%(asctime)s] %(name)s: %(message)s', '%Y%m%d:%H%M%S'))
+ l.addHandler(lh)
+
+host_key = secsh.RSAKey()
+host_key.read_private_key_file('/home/robey/sshkey/ssh_host_rsa_key')
+
+# now connect
+try:
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ 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)
+
+try:
+ sock.listen(100)
+ client, addr = sock.accept()
+except Exception, e:
+ print '*** Listen/accept failed: ' + str(e)
+ traceback.print_exc()
+ sys.exit(1)
+
+try:
+ event = threading.Event()
+ t = secsh.Transport(client)
+ t.add_server_key(host_key)
+ t.ultra_debug = 1
+ t.start_server(event)
+ # print repr(t)
+ event.wait(10)
+ if not t.is_active():
+ print '*** SSH negotiation failed.'
+ sys.exit(1)
+ # print repr(t)
+except Exception, e:
+ print '*** Caught exception: ' + str(e.__class__) + ': ' + str(e)
+ traceback.print_exc()
+ try:
+ t.close()
+ except:
+ pass
+ sys.exit(1)
+
diff --git a/demo.py b/demo.py
new file mode 100755
index 00000000..fc707e4a
--- /dev/null
+++ b/demo.py
@@ -0,0 +1,180 @@
+#!/usr/bin/python
+
+import sys, os, socket, threading, getpass, logging, time, base64, select, termios, tty, traceback
+import secsh
+
+
+##### utility functions
+
+def load_host_keys():
+ filename = os.environ['HOME'] + '/.ssh/known_hosts'
+ keys = {}
+ try:
+ f = open(filename, 'r')
+ except Exception, e:
+ print '*** Unable to open host keys file (%s)' % filename
+ return
+ for line in f:
+ keylist = line.split(' ')
+ if len(keylist) != 3:
+ continue
+ hostlist, keytype, key = keylist
+ hosts = hostlist.split(',')
+ for host in hosts:
+ if not keys.has_key(host):
+ keys[host] = {}
+ keys[host][keytype] = base64.decodestring(key)
+ f.close()
+ return keys
+
+
+##### main demo
+
+# setup logging
+l = logging.getLogger("secsh")
+l.setLevel(logging.DEBUG)
+if len(l.handlers) == 0:
+ f = open('demo.log', 'w')
+ lh = logging.StreamHandler(f)
+ 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]
+ if hostname.find('@') >= 0:
+ username, hostname = hostname.split('@')
+else:
+ hostname = raw_input('Hostname: ')
+if len(hostname) == 0:
+ print '*** Hostname required.'
+ sys.exit(1)
+port = 22
+if hostname.find(':') >= 0:
+ hostname, portstr = hostname.split(':')
+ port = int(portstr)
+
+# now connect
+try:
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.connect((hostname, port))
+except Exception, e:
+ print '*** Connect failed: ' + str(e)
+ traceback.print_exc()
+ sys.exit(1)
+
+try:
+ event = threading.Event()
+ t = secsh.Transport(sock)
+ t.ultra_debug = 1
+ t.start_client(event)
+ # print repr(t)
+ event.wait(10)
+ if not t.is_active():
+ print '*** SSH negotiation failed.'
+ sys.exit(1)
+ # print repr(t)
+
+ keys = load_host_keys()
+ keytype, hostkey = t.get_host_key()
+ if not keys.has_key(hostname):
+ print '*** WARNING: Unknown host key!'
+ elif not keys[hostname].has_key(keytype):
+ print '*** WARNING: Unknown host key!'
+ elif keys[hostname][keytype] != hostkey:
+ print '*** WARNING: Host key has changed!!!'
+ sys.exit(1)
+ else:
+ print '*** Host key OK.'
+
+ event.clear()
+
+ # get username
+ if username == '':
+ default_username = getpass.getuser()
+ username = raw_input('Username [%s]: ' % default_username)
+ if len(username) == 0:
+ username = default_username
+
+ # ask for what kind of authentication to try
+ default_auth = 'p'
+ auth = raw_input('Auth by (p)assword, (r)sa key, or (d)ss key? [%s] ' % default_auth)
+ if len(auth) == 0:
+ auth = default_auth
+
+ if auth == 'r':
+ key = secsh.RSAKey()
+ default_path = os.environ['HOME'] + '/.ssh/id_rsa'
+ path = raw_input('RSA key [%s]: ' % default_path)
+ if len(path) == 0:
+ path = default_path
+ key.read_private_key_file(path)
+ t.auth_key(username, key, event)
+ elif auth == 'd':
+ key = secsh.DSSKey()
+ default_path = os.environ['HOME'] + '/.ssh/id_dsa'
+ path = raw_input('DSS key [%s]: ' % default_path)
+ if len(path) == 0:
+ path = default_path
+ key.read_private_key_file(path)
+ t.auth_key(username, key, event)
+ else:
+ pw = getpass.getpass('Password for %s@%s: ' % (username, hostname))
+ t.auth_password(username, pw, event)
+
+ event.wait(10)
+ # print repr(t)
+ if not t.is_authenticated():
+ print '*** Authentication failed. :('
+ t.close()
+ sys.exit(1)
+
+ chan = t.open_session()
+ chan.get_pty()
+ chan.invoke_shell()
+ print '*** Here we go!'
+ print
+
+ try:
+ oldtty = termios.tcgetattr(sys.stdin)
+ tty.setraw(sys.stdin.fileno())
+ tty.setcbreak(sys.stdin.fileno())
+ chan.settimeout(0.0)
+
+ while 1:
+ r, w, e = select.select([chan, sys.stdin], [], [])
+ if chan in r:
+ try:
+ x = chan.recv(1024)
+ if len(x) == 0:
+ print
+ print '*** EOF\r\n',
+ break
+ sys.stdout.write(x)
+ sys.stdout.flush()
+ except socket.timeout:
+ pass
+ if sys.stdin in r:
+ # FIXME: reading 1 byte at a time is incredibly dumb.
+ x = sys.stdin.read(1)
+ if len(x) == 0:
+ print
+ print '*** Bye.\r\n',
+ break
+ chan.send(x)
+
+ finally:
+ termios.tcsetattr(sys.stdin, termios.TCSADRAIN, oldtty)
+
+ chan.close()
+ t.close()
+
+except Exception, e:
+ print '*** Caught exception: ' + str(e.__class__) + ': ' + str(e)
+ traceback.print_exc()
+ try:
+ t.close()
+ except:
+ pass
+ sys.exit(1)
+
diff --git a/dsskey.py b/dsskey.py
new file mode 100644
index 00000000..c2031291
--- /dev/null
+++ b/dsskey.py
@@ -0,0 +1,121 @@
+#!/usr/bin/python
+
+import base64
+from message import Message
+from transport import MSG_USERAUTH_REQUEST
+from util import inflate_long, deflate_long
+from Crypto.PublicKey import DSA
+from Crypto.Hash import SHA
+from ber import BER
+
+from util import format_binary
+
+
+class DSSKey(object):
+
+ def __init__(self, msg=None):
+ self.valid = 0
+ if (msg == None) or (msg.get_string() != 'ssh-dss'):
+ return
+ self.p = msg.get_mpint()
+ self.q = msg.get_mpint()
+ self.g = msg.get_mpint()
+ self.y = msg.get_mpint()
+ self.size = len(deflate_long(self.p, 0))
+ self.valid = 1
+
+ def __str__(self):
+ if not self.valid:
+ return ''
+ m = Message()
+ m.add_string('ssh-dss')
+ m.add_mpint(self.p)
+ m.add_mpint(self.q)
+ m.add_mpint(self.g)
+ m.add_mpint(self.y)
+ return str(m)
+
+ def get_name(self):
+ return 'ssh-dss'
+
+ def verify_ssh_sig(self, data, msg):
+ if not self.valid:
+ return 0
+ if len(str(msg)) == 40:
+ # spies.com bug: signature has no header
+ sig = str(msg)
+ else:
+ kind = msg.get_string()
+ if kind != 'ssh-dss':
+ return 0
+ sig = msg.get_string()
+
+ # pull out (r, s) which are NOT encoded as mpints
+ sigR = inflate_long(sig[:20], 1)
+ sigS = inflate_long(sig[20:], 1)
+ sigM = inflate_long(SHA.new(data).digest(), 1)
+
+ dss = DSA.construct((long(self.y), long(self.g), long(self.p), long(self.q)))
+ return dss.verify(sigM, (sigR, sigS))
+
+ def sign_ssh_data(self, data):
+ hash = SHA.new(data).digest()
+ dss = DSA.construct((long(self.y), long(self.g), long(self.p), long(self.q), long(self.x)))
+ # generate a suitable k
+ qsize = len(deflate_long(self.q, 0))
+ while 1:
+ k = inflate_long(randpool.get_bytes(qsize), 1)
+ if (k > 2) and (k < self.q):
+ break
+ r, s = dss.sign(inflate_long(hash, 1), k)
+ m = Message()
+ m.add_string('ssh-dss')
+ m.add_string(deflate_long(r, 0) + deflate_long(s, 0))
+ return str(m)
+
+
+ rsa = RSA.construct((long(self.n), long(self.e), long(self.d)))
+ sig = deflate_long(rsa.sign(self.pkcs1imify(hash), '')[0], 0)
+ m = Message()
+ m.add_string('ssh-rsa')
+ m.add_string(sig)
+ return str(m)
+
+ def read_private_key_file(self, filename):
+ # private key file contains:
+ # DSAPrivateKey = { version = 0, p, q, g, y, x }
+ self.valid = 0
+ try:
+ f = open(filename, 'r')
+ lines = f.readlines()
+ f.close()
+ except:
+ return
+ if lines[0].strip() != '-----BEGIN DSA PRIVATE KEY-----':
+ return
+ try:
+ data = base64.decodestring(''.join(lines[1:-1]))
+ except:
+ return
+ keylist = BER(data).decode()
+ if (type(keylist) != type([])) or (len(keylist) < 6) or (keylist[0] != 0):
+ return
+ self.p = keylist[1]
+ self.q = keylist[2]
+ self.g = keylist[3]
+ self.y = keylist[4]
+ self.x = keylist[5]
+ self.size = len(deflate_long(self.p, 0))
+ self.valid = 1
+
+ def sign_ssh_session(self, randpool, sid, username):
+ m = Message()
+ m.add_string(sid)
+ m.add_byte(chr(MSG_USERAUTH_REQUEST))
+ m.add_string(username)
+ m.add_string('ssh-connection')
+ m.add_string('publickey')
+ m.add_boolean(1)
+ m.add_string('ssh-dss')
+ m.add_string(str(self))
+ return self.sign_ssh_data(str(m))
diff --git a/kex_gex.py b/kex_gex.py
new file mode 100644
index 00000000..01e74f2b
--- /dev/null
+++ b/kex_gex.py
@@ -0,0 +1,180 @@
+#!/usr/bin/python
+
+# variant on group1 (see kex_group1.py) where the prime "p" and generator "g"
+# are provided by the server. a bit more work is required on our side (and a
+# LOT more on the server side).
+
+from message import Message, inflate_long, deflate_long
+from secsh import SSHException
+from transport import MSG_NEWKEYS
+from Crypto.Hash import SHA
+from Crypto.Util import number
+from logging import DEBUG
+
+MSG_KEXDH_GEX_GROUP, MSG_KEXDH_GEX_INIT, MSG_KEXDH_GEX_REPLY, MSG_KEXDH_GEX_REQUEST = range(31, 35)
+
+
+class KexGex(object):
+
+ name = 'diffie-hellman-group-exchange-sha1'
+ min_bits = 1024
+ max_bits = 8192
+ preferred_bits = 2048
+
+ def __init__(self, transport):
+ self.transport = transport
+
+ def start_kex(self):
+ if self.transport.server_mode:
+ self.transport.expected_packet = MSG_KEXDH_GEX_REQUEST
+ return
+ # request a bit range: we accept (min_bits) to (max_bits), but prefer
+ # (preferred_bits). according to the spec, we shouldn't pull the
+ # minimum up above 1024.
+ m = Message()
+ m.add_byte(chr(MSG_KEXDH_GEX_REQUEST))
+ m.add_int(self.min_bits)
+ m.add_int(self.preferred_bits)
+ m.add_int(self.max_bits)
+ self.transport.send_message(m)
+ self.transport.expected_packet = MSG_KEXDH_GEX_GROUP
+
+ def parse_next(self, ptype, m):
+ if ptype == MSG_KEXDH_GEX_REQUEST:
+ return self.parse_kexdh_gex_request(m)
+ elif ptype == MSG_KEXDH_GEX_GROUP:
+ return self.parse_kexdh_gex_group(m)
+ elif ptype == MSG_KEXDH_GEX_INIT:
+ return self.parse_kexdh_gex_init(m)
+ elif ptype == MSG_KEXDH_GEX_REPLY:
+ return self.parse_kexdh_gex_reply(m)
+ raise SSHException('KexGex asked to handle packet type %d' % ptype)
+
+ def bit_length(n):
+ norm = deflate_long(n, 0)
+ hbyte = ord(norm[0])
+ bitlen = len(norm) * 8
+ while not (hbyte & 0x80):
+ hbyte <<= 1
+ bitlen -= 1
+ return bitlen
+ bit_length = staticmethod(bit_length)
+
+ def generate_x(self):
+ # generate an "x" (1 < x < (p-1)/2).
+ q = (self.p - 1) // 2
+ qnorm = deflate_long(q, 0)
+ qhbyte = ord(qnorm[0])
+ bytes = len(qnorm)
+ qmask = 0xff
+ while not (qhbyte & 0x80):
+ qhbyte <<= 1
+ qmask >>= 1
+ while 1:
+ self.transport.randpool.stir()
+ x_bytes = self.transport.randpool.get_bytes(bytes)
+ x_bytes = chr(ord(x_bytes[0]) & qmask) + x_bytes[1:]
+ x = inflate_long(x_bytes, 1)
+ if (x > 1) and (x < q):
+ break
+ self.x = x
+
+ def parse_kexdh_gex_request(self, m):
+ min = m.get_int()
+ preferred = m.get_int()
+ max = m.get_int()
+ # smoosh the user's preferred size into our own limits
+ if preferred > self.max_bits:
+ preferred = self.max_bits
+ if preferred < self.min_bits:
+ preferred = self.min_bits
+ # now save a copy
+ self.min_bits = min
+ self.preferred_bits = preferred
+ self.max_bits = max
+ # generate prime
+ while 1:
+ self.transport.log(DEBUG, 'stir...')
+ self.transport.randpool.stir()
+ self.transport.log(DEBUG, 'get-prime %d...' % preferred)
+ self.p = number.getRandomNumber(preferred, self.transport.randpool.get_bytes)
+ self.transport.log(DEBUG, 'got ' + repr(self.p))
+ if number.isPrime((self.p - 1) // 2):
+ break
+ self.g = 2
+ m = Message()
+ m.add_byte(chr(MSG_KEXDH_GEX_GROUP))
+ m.add_mpint(self.p)
+ m.add_mpint(self.g)
+ self.transport.send_message(m)
+ self.transport.expected_packet = MSG_KEXDH_GEX_INIT
+
+ def parse_kexdh_gex_group(self, m):
+ self.p = m.get_mpint()
+ self.g = m.get_mpint()
+ # reject if p's bit length < 1024 or > 8192
+ bitlen = self.bit_length(self.p)
+ if (bitlen < 1024) or (bitlen > 8192):
+ raise SSHException('Server-generated gex p (don\'t ask) is out of range (%d bits)' % bitlen)
+ self.transport.log(DEBUG, 'Got server p (%d bits)' % bitlen)
+ self.generate_x()
+ # now compute e = g^x mod p
+ self.e = pow(self.g, self.x, self.p)
+ m = Message()
+ m.add_byte(chr(MSG_KEXDH_GEX_INIT))
+ m.add_mpint(self.e)
+ self.transport.send_message(m)
+ self.transport.expected_packet = MSG_KEXDH_GEX_REPLY
+
+ def parse_kexdh_gex_init(self, m):
+ self.e = m.get_mpint()
+ if (self.e < 1) or (self.e > self.p - 1):
+ raise SSHException('Client kex "e" is out of range')
+ self.generate_x()
+ K = pow(self.e, self.x, P)
+ key = str(self.transport.get_server_key())
+ # okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || min || n || max || p || g || e || f || K)
+ hm = Message().add(self.transport.remote_version).add(self.transport.local_version)
+ hm.add(self.transport.remote_kex_init).add(self.transport.local_kex_init).add(key)
+ hm.add_int(self.min_bits)
+ hm.add_int(self.preferred_bits)
+ hm.add_int(self.max_bits)
+ hm.add_mpint(self.p)
+ hm.add_mpint(self.g)
+ hm.add(self.e).add(self.f).add(K)
+ H = SHA.new(str(hm)).digest()
+ self.transport.set_K_H(K, H)
+ # sign it
+ sig = self.transport.get_server_key().sign_ssh_data(H)
+ # send reply
+ m = Message()
+ m.add_byte(chr(MSG_KEXDH_GEX_REPLY))
+ m.add_string(key)
+ m.add_mpint(self.f)
+ m.add_string(sig)
+ self.transport.send_message(m)
+ self.transport.activate_outbound()
+ self.transport.expected_packet = MSG_NEWKEYS
+
+ def parse_kexdh_gex_reply(self, m):
+ host_key = m.get_string()
+ self.f = m.get_mpint()
+ sig = m.get_string()
+ if (self.f < 1) or (self.f > self.p - 1):
+ raise SSHException('Server kex "f" is out of range')
+ K = pow(self.f, self.x, self.p)
+ # okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || min || n || max || p || g || e || f || K)
+ hm = Message().add(self.transport.local_version).add(self.transport.remote_version)
+ hm.add(self.transport.local_kex_init).add(self.transport.remote_kex_init).add(host_key)
+ hm.add_int(self.min_bits)
+ hm.add_int(self.preferred_bits)
+ hm.add_int(self.max_bits)
+ hm.add_mpint(self.p)
+ hm.add_mpint(self.g)
+ hm.add(self.e).add(self.f).add(K)
+ self.transport.set_K_H(K, SHA.new(str(hm)).digest())
+ self.transport.verify_key(host_key, sig)
+ self.transport.activate_outbound()
+ self.transport.expected_packet = MSG_NEWKEYS
+
+
diff --git a/kex_group1.py b/kex_group1.py
new file mode 100644
index 00000000..7d65c388
--- /dev/null
+++ b/kex_group1.py
@@ -0,0 +1,104 @@
+#!/usr/bin/python
+
+# standard SSH key exchange ("kex" if you wanna sound cool):
+# diffie-hellman of 1024 bit key halves, using a known "p" prime and
+# "g" generator.
+
+from message import Message, inflate_long
+from secsh import SSHException
+from transport import MSG_NEWKEYS
+from Crypto.Hash import SHA
+from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL
+
+MSG_KEXDH_INIT, MSG_KEXDH_REPLY = range(30, 32)
+
+# draft-ietf-secsh-transport-09.txt, page 17
+P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFFL
+G = 2
+
+
+class KexGroup1(object):
+
+ name = 'diffie-hellman-group1-sha1'
+
+ def __init__(self, transport):
+ self.transport = transport
+
+ def generate_x(self):
+ # generate an "x" (1 < x < q), where q is (p-1)/2.
+ # p is a 128-byte (1024-bit) number, where the first 64 bits are 1.
+ # therefore q can be approximated as a 2^1023. we drop the subset of
+ # potential x where the first 63 bits are 1, because some of those will be
+ # larger than q (but this is a tiny tiny subset of potential x).
+ while 1:
+ self.transport.randpool.stir()
+ x_bytes = self.transport.randpool.get_bytes(128)
+ x_bytes = chr(ord(x_bytes[0]) & 0x7f) + x_bytes[1:]
+ if (x_bytes[:8] != '\x7F\xFF\xFF\xFF\xFF\xFF\xFF\xFF') and \
+ (x_bytes[:8] != '\x00\x00\x00\x00\x00\x00\x00\x00'):
+ break
+ self.x = inflate_long(x_bytes)
+
+ def start_kex(self):
+ self.generate_x()
+ if self.transport.server_mode:
+ # compute f = g^x mod p, but don't send it yet
+ self.f = pow(G, self.x, P)
+ self.transport.expected_packet = MSG_KEXDH_INIT
+ return
+ # compute e = g^x mod p (where g=2), and send it
+ self.e = pow(G, self.x, P)
+ m = Message()
+ m.add_byte(chr(MSG_KEXDH_INIT))
+ m.add_mpint(self.e)
+ self.transport.send_message(m)
+ self.transport.expected_packet = MSG_KEXDH_REPLY
+
+ def parse_next(self, ptype, m):
+ if self.transport.server_mode and (ptype == MSG_KEXDH_INIT):
+ return self.parse_kexdh_init(m)
+ elif not self.transport.server_mode and (ptype == MSG_KEXDH_REPLY):
+ return self.parse_kexdh_reply(m)
+ raise SSHException('KexGroup1 asked to handle packet type %d' % ptype)
+
+ def parse_kexdh_reply(self, m):
+ # client mode
+ host_key = m.get_string()
+ self.f = m.get_mpint()
+ if (self.f < 1) or (self.f > P - 1):
+ raise SSHException('Server kex "f" is out of range')
+ sig = m.get_string()
+ K = pow(self.f, self.x, P)
+ # okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || e || f || K)
+ hm = Message().add(self.transport.local_version).add(self.transport.remote_version)
+ hm.add(self.transport.local_kex_init).add(self.transport.remote_kex_init).add(host_key)
+ hm.add(self.e).add(self.f).add(K)
+ self.transport.set_K_H(K, SHA.new(str(hm)).digest())
+ self.transport.verify_key(host_key, sig)
+ self.transport.activate_outbound()
+ self.transport.expected_packet = MSG_NEWKEYS
+
+ def parse_kexdh_init(self, m):
+ # server mode
+ self.e = m.get_mpint()
+ if (self.e < 1) or (self.e > P - 1):
+ raise SSHException('Client kex "e" is out of range')
+ K = pow(self.e, self.x, P)
+ key = str(self.transport.get_server_key())
+ # okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || e || f || K)
+ hm = Message().add(self.transport.remote_version).add(self.transport.local_version)
+ hm.add(self.transport.remote_kex_init).add(self.transport.local_kex_init).add(key)
+ hm.add(self.e).add(self.f).add(K)
+ H = SHA.new(str(hm)).digest()
+ self.transport.set_K_H(K, H)
+ # sign it
+ sig = self.transport.get_server_key().sign_ssh_data(H)
+ # send reply
+ m = Message()
+ m.add_byte(chr(MSG_KEXDH_REPLY))
+ m.add_string(key)
+ m.add_mpint(self.f)
+ m.add_string(sig)
+ self.transport.send_message(m)
+ self.transport.activate_outbound()
+ self.transport.expected_packet = MSG_NEWKEYS
diff --git a/message.py b/message.py
new file mode 100644
index 00000000..aeccd9f4
--- /dev/null
+++ b/message.py
@@ -0,0 +1,119 @@
+# implementation of a secsh "message"
+
+import string, types, struct
+from util import inflate_long, deflate_long
+
+
+class Message(object):
+ "represents the encoding of a secsh message"
+
+ def __init__(self, content=''):
+ self.packet = content
+ self.idx = 0
+ self.seqno = -1
+
+ def __str__(self):
+ return self.packet
+
+ def __repr__(self):
+ return 'Message(' + repr(self.packet) + ')'
+
+ def get_remainder(self):
+ "remaining bytes still unparsed"
+ return self.packet[self.idx:]
+
+ def get_so_far(self):
+ "bytes that have been parsed"
+ return self.packet[:self.idx]
+
+ def get_bytes(self, n):
+ if self.idx + n > len(self.packet):
+ return '\x00'*n
+ b = self.packet[self.idx:self.idx+n]
+ self.idx = self.idx + n
+ return b
+
+ def get_byte(self):
+ return self.get_bytes(1)
+
+ def get_boolean(self):
+ b = self.get_bytes(1)
+ if b == '\x00':
+ return 0
+ else:
+ return 1
+
+ def get_int(self):
+ x = self.packet
+ i = self.idx
+ if i + 4 > len(x):
+ return 0
+ n = struct.unpack('>I', x[i:i+4])[0]
+ self.idx = i+4
+ return n
+
+ def get_mpint(self):
+ return inflate_long(self.get_string())
+
+ def get_string(self):
+ l = self.get_int()
+ if self.idx + l > len(self.packet):
+ return ''
+ str = self.packet[self.idx:self.idx+l]
+ self.idx = self.idx + l
+ return str
+
+ def get_list(self):
+ str = self.get_string()
+ l = string.split(str, ',')
+ return l
+
+ def add_bytes(self, b):
+ self.packet = self.packet + b
+ return self
+
+ def add_byte(self, b):
+ self.packet = self.packet + b
+ return self
+
+ def add_boolean(self, b):
+ if b:
+ self.add_byte('\x01')
+ else:
+ self.add_byte('\x00')
+ return self
+
+ def add_int(self, n):
+ self.packet = self.packet + struct.pack('>I', n)
+ return self
+
+ def add_mpint(self, z):
+ "this only works on positive numbers"
+ self.add_string(deflate_long(z))
+ return self
+
+ def add_string(self, s):
+ self.add_int(len(s))
+ self.packet = self.packet + s
+ return self
+
+ def add_list(self, l):
+ out = string.join(l, ',')
+ self.add_int(len(out))
+ self.packet = self.packet + out
+ return self
+
+ def add(self, i):
+ if type(i) == types.StringType:
+ return self.add_string(i)
+ elif type(i) == types.IntType:
+ return self.add_int(i)
+ elif type(i) == types.LongType:
+ if i > 0xffffffffL:
+ return self.add_mpint(i)
+ else:
+ return self.add_int(i)
+ elif type(i) == types.ListType:
+ return self.add_list(i)
+ else:
+ raise exception('Unknown type')
diff --git a/rsakey.py b/rsakey.py
new file mode 100644
index 00000000..49c1c285
--- /dev/null
+++ b/rsakey.py
@@ -0,0 +1,102 @@
+#!/usr/bin/python
+
+from message import Message
+from transport import MSG_USERAUTH_REQUEST
+from Crypto.PublicKey import RSA
+from Crypto.Hash import SHA
+from ber import BER
+from util import format_binary, inflate_long, deflate_long
+import base64
+
+class RSAKey(object):
+
+ def __init__(self, msg=None):
+ self.valid = 0
+ if (msg == None) or (msg.get_string() != 'ssh-rsa'):
+ return
+ self.e = msg.get_mpint()
+ self.n = msg.get_mpint()
+ self.size = len(deflate_long(self.n, 0))
+ self.valid = 1
+
+ def __str__(self):
+ if not self.valid:
+ return ''
+ m = Message()
+ m.add_string('ssh-rsa')
+ m.add_mpint(self.e)
+ m.add_mpint(self.n)
+ return str(m)
+
+ def get_name(self):
+ return 'ssh-rsa'
+
+ def pkcs1imify(self, data):
+ """
+ turn a 20-byte SHA1 hash into a blob of data as large as the key's N,
+ using PKCS1's \"emsa-pkcs1-v1_5\" encoding. totally bizarre.
+ """
+ SHA1_DIGESTINFO = '\x30\x21\x30\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14'
+ filler = '\xff' * (self.size - len(SHA1_DIGESTINFO) - len(data) - 3)
+ return '\x00\x01' + filler + '\x00' + SHA1_DIGESTINFO + data
+
+ def verify_ssh_sig(self, data, msg):
+ if (not self.valid) or (msg.get_string() != 'ssh-rsa'):
+ return 0
+ sig = inflate_long(msg.get_string(), 1)
+ # verify the signature by SHA'ing the data and encrypting it using the
+ # public key. some wackiness ensues where we "pkcs1imify" the 20-byte
+ # hash into a string as long as the RSA key.
+ hash = inflate_long(self.pkcs1imify(SHA.new(data).digest()), 1)
+ rsa = RSA.construct((long(self.n), long(self.e)))
+ return rsa.verify(hash, (sig,))
+
+ def sign_ssh_data(self, data):
+ hash = SHA.new(data).digest()
+ rsa = RSA.construct((long(self.n), long(self.e), long(self.d)))
+ sig = deflate_long(rsa.sign(self.pkcs1imify(hash), '')[0], 0)
+ m = Message()
+ m.add_string('ssh-rsa')
+ m.add_string(sig)
+ return str(m)
+
+ def read_private_key_file(self, filename):
+ # 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
+ try:
+ f = open(filename, 'r')
+ lines = f.readlines()
+ f.close()
+ except:
+ return
+ if lines[0].strip() != '-----BEGIN RSA PRIVATE KEY-----':
+ return
+ try:
+ data = base64.decodestring(''.join(lines[1:-1]))
+ except:
+ return
+ keylist = BER(data).decode()
+ if (type(keylist) != type([])) or (len(keylist) < 4) or (keylist[0] != 0):
+ return
+ self.n = keylist[1]
+ self.e = keylist[2]
+ self.d = keylist[3]
+ # not really needed
+ self.p = keylist[4]
+ self.q = keylist[5]
+ self.size = len(deflate_long(self.n, 0))
+ self.valid = 1
+
+ def sign_ssh_session(self, randpool, sid, username):
+ m = Message()
+ m.add_string(sid)
+ m.add_byte(chr(MSG_USERAUTH_REQUEST))
+ m.add_string(username)
+ m.add_string('ssh-connection')
+ m.add_string('publickey')
+ m.add_boolean(1)
+ m.add_string('ssh-rsa')
+ m.add_string(str(self))
+ return self.sign_ssh_data(str(m))
+
diff --git a/secsh.py b/secsh.py
new file mode 100644
index 00000000..6f124947
--- /dev/null
+++ b/secsh.py
@@ -0,0 +1,23 @@
+#!/usr/bin/python
+
+import sys
+
+if (sys.version_info[0] < 2) or ((sys.version_info[0] == 2) and (sys.version_info[1] < 3)):
+ raise RuntimeError('You need python 2.3 for this module.')
+
+# FIXME rename
+class SSHException(Exception):
+ pass
+
+
+from auth_transport import Transport
+from channel import Channel
+from rsakey import RSAKey
+from dsskey import DSSKey
+
+
+__author__ = "Robey Pointer <robey@lag.net>"
+__date__ = "18 Sep 2003"
+__version__ = "0.1-bulbasaur"
+__credits__ = "Huzzah!"
+
diff --git a/setup.py b/setup.py
new file mode 100644
index 00000000..64b3401f
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,30 @@
+from distutils.core import setup
+
+longdesc = '''
+This is a library for making client-side SSH2 connections (server-side is
+coming soon). All major ciphers and hash methods are supported.
+
+Required packages:
+ pyCrypto
+'''
+
+setup(name = "secsh",
+ version = "0.1-bulbasaur",
+ description = "SSH2 protocol library",
+ author = "Robey Pointer",
+ author_email = "robey@lag.net",
+ url = "http://www.lag.net/~robey/secsh/",
+ py_modules = [ 'secsh', 'transport', 'channel', 'message', 'util', 'ber',
+ 'kex_group1', 'kex_gex', 'rsakey', 'dsskey' ],
+ scripts = [ 'demo.py' ],
+ download_url = 'http://www.lag.net/~robey/secsh/secsh-0.1-bulbasaur.zip',
+ license = 'LGPL',
+ platforms = 'Posix; MacOS X; Windows',
+ classifiers = [ 'Development Status :: 3 - Alpha',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)',
+ 'Operating System :: OS Independent',
+ 'Topic :: Internet',
+ 'Topic :: Security :: Cryptography' ],
+ long_description = longdesc,
+ )
diff --git a/transport.py b/transport.py
new file mode 100644
index 00000000..9790f2b6
--- /dev/null
+++ b/transport.py
@@ -0,0 +1,758 @@
+#!/usr/bin/python
+
+MSG_DISCONNECT, MSG_IGNORE, MSG_UNIMPLEMENTED, MSG_DEBUG, MSG_SERVICE_REQUEST, \
+ MSG_SERVICE_ACCEPT = range(1, 7)
+MSG_KEXINIT, MSG_NEWKEYS = range(20, 22)
+MSG_USERAUTH_REQUEST, MSG_USERAUTH_FAILURE, MSG_USERAUTH_SUCCESS, \
+ MSG_USERAUTH_BANNER = range(50, 54)
+MSG_USERAUTH_PK_OK = 60
+MSG_CHANNEL_OPEN, MSG_CHANNEL_OPEN_SUCCESS, MSG_CHANNEL_OPEN_FAILURE, \
+ MSG_CHANNEL_WINDOW_ADJUST, MSG_CHANNEL_DATA, MSG_CHANNEL_EXTENDED_DATA, \
+ MSG_CHANNEL_EOF, MSG_CHANNEL_CLOSE, MSG_CHANNEL_REQUEST, \
+ MSG_CHANNEL_SUCCESS, MSG_CHANNEL_FAILURE = range(90, 101)
+
+
+import sys, os, string, threading, socket, logging, struct
+from message import Message
+from channel import Channel
+from secsh import SSHException
+from util import format_binary, safe_string, inflate_long, deflate_long
+from rsakey import RSAKey
+from dsskey import DSSKey
+from kex_group1 import KexGroup1
+from kex_gex import KexGex
+
+# these come from PyCrypt
+# http://www.amk.ca/python/writing/pycrypt/
+# i believe this on the standards track.
+# PyCrypt compiled for Win32 can be downloaded from the HashTar homepage:
+# http://nitace.bsd.uchicago.edu:8080/hashtar
+from Crypto.Util.randpool import PersistentRandomPool, RandomPool
+from Crypto.Cipher import Blowfish, AES, DES3
+from Crypto.Hash import SHA, MD5, HMAC
+from Crypto.PublicKey import RSA
+
+from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL
+
+
+# channel request failed reasons:
+CONNECTION_FAILED_CODE = {
+ 1: 'Administratively prohibited',
+ 2: 'Connect failed',
+ 3: 'Unknown channel type',
+ 4: 'Resource shortage'
+}
+
+
+# keep a crypto-strong PRNG nearby
+try:
+ randpool = PersistentRandomPool(os.getenv('HOME') + '/.randpool')
+except:
+ # the above will likely fail on Windows - fall back to non-persistent random pool
+ randpool = RandomPool()
+
+randpool.randomize()
+
+
+class BaseTransport(threading.Thread):
+ '''
+ An SSH Transport attaches to a stream (usually a socket), negotiates an
+ encrypted session, authenticates, and then creates stream tunnels, called
+ "channels", across the session. Multiple channels can be multiplexed
+ across a single session (and often are, in the case of port forwardings).
+
+ Transport expects to receive a "socket-like object" to talk to the SSH
+ server. This means it has a method "settimeout" which sets a timeout for
+ read/write calls, and a method "send()" to write bytes and "recv()" to
+ read bytes. "recv" returns from 1 to n bytes, or 0 if the stream has been
+ closed. EOFError may also be raised on a closed stream. (A return value
+ of 0 is converted to an EOFError internally.) "send(s)" writes from 1 to
+ len(s) bytes, and returns the number of bytes written, or returns 0 if the
+ stream has been closed. As with instream, EOFError may be raised instead
+ of returning 0.
+
+ FIXME: Describe events here.
+ '''
+
+ PROTO_ID = '2.0'
+ CLIENT_ID = 'pyssh_1.1'
+
+ preferred_ciphers = [ 'aes128-cbc', 'blowfish-cbc', 'aes256-cbc', '3des-cbc' ]
+ preferred_macs = [ 'hmac-sha1', 'hmac-md5', 'hmac-sha1-96', 'hmac-md5-96' ]
+ preferred_keys = [ 'ssh-rsa', 'ssh-dss' ]
+ preferred_kex = [ 'diffie-hellman-group1-sha1', 'diffie-hellman-group-exchange-sha1' ]
+
+ cipher_info = {
+ 'blowfish-cbc': { 'class': Blowfish, 'mode': Blowfish.MODE_CBC, 'block-size': 8, 'key-size': 16 },
+ 'aes128-cbc': { 'class': AES, 'mode': AES.MODE_CBC, 'block-size': 16, 'key-size': 16 },
+ 'aes256-cbc': { 'class': AES, 'mode': AES.MODE_CBC, 'block-size': 16, 'key-size': 32 },
+ '3des-cbc': { 'class': DES3, 'mode': DES3.MODE_CBC, 'block-size': 8, 'key-size': 24 },
+ }
+
+ mac_info = {
+ 'hmac-sha1': { 'class': SHA, 'size': 20 },
+ 'hmac-sha1-96': { 'class': SHA, 'size': 12 },
+ 'hmac-md5': { 'class': MD5, 'size': 16 },
+ 'hmac-md5-96': { 'class': MD5, 'size': 12 },
+ }
+
+ kex_info = {
+ 'diffie-hellman-group1-sha1': KexGroup1,
+ 'diffie-hellman-group-exchange-sha1': KexGex,
+ }
+
+ REKEY_PACKETS = pow(2, 30)
+ REKEY_BYTES = pow(2, 30)
+
+ def __init__(self, sock):
+ threading.Thread.__init__(self)
+ self.randpool = randpool
+ self.sock = sock
+ self.sock.settimeout(0.1)
+ # negotiated crypto parameters
+ self.local_version = 'SSH-' + self.PROTO_ID + '-' + self.CLIENT_ID
+ self.remote_version = ''
+ self.block_size_out = self.block_size_in = 8
+ self.local_mac_len = self.remote_mac_len = 0
+ self.engine_in = self.engine_out = None
+ self.local_cipher = self.remote_cipher = ''
+ self.sequence_number_in = self.sequence_number_out = 0L
+ self.local_kex_init = self.remote_kex_init = None
+ self.session_id = None
+ # /negotiated crypto parameters
+ self.expected_packet = 0
+ self.active = 0
+ self.initial_kex_done = 0
+ self.write_lock = threading.Lock() # lock around outbound writes (packet computation)
+ self.lock = threading.Lock() # synchronization (always higher level than write_lock)
+ self.authenticated = 0
+ self.channels = { } # (id -> Channel)
+ self.channel_events = { } # (id -> Event)
+ self.channel_counter = 1
+ self.logger = logging.getLogger('secsh.transport')
+ self.window_size = 65536
+ self.max_packet_size = 2048
+ self.ultra_debug = 0
+ # used for noticing when to re-key:
+ self.received_bytes = 0
+ self.received_packets = 0
+ self.received_packets_overflow = 0
+ # user-defined event callbacks:
+ self.completion_event = None
+ # server mode:
+ self.server_mode = 0
+ self.server_key_dict = { }
+
+ def start_client(self, event=None):
+ self.completion_event = event
+ self.start()
+
+ def start_server(self, event=None):
+ self.server_mode = 1
+ self.completion_event = event
+ self.start()
+
+ def add_server_key(self, key):
+ self.server_key_dict[key.get_name()] = key
+
+ def get_server_key(self):
+ try:
+ return self.server_key_dict[self.host_key_type]
+ except KeyError:
+ return None
+
+ def __repr__(self):
+ if not self.active:
+ return '<secsh.Transport (unconnected)>'
+ out = '<sesch.Transport'
+ #if self.remote_version != '':
+ # out += ' (server version "%s")' % self.remote_version
+ if self.local_cipher != '':
+ out += ' (cipher %s)' % self.local_cipher
+ if self.authenticated:
+ if len(self.channels) == 1:
+ out += ' (active; 1 open channel)'
+ else:
+ out += ' (active; %d open channels)' % len(self.channels)
+ elif self.initial_kex_done:
+ out += ' (connected; awaiting auth)'
+ else:
+ out += ' (connecting)'
+ out += '>'
+ return out
+
+ def log(self, level, msg):
+ if type(msg) == type([]):
+ for m in msg:
+ self.logger.log(level, m)
+ else:
+ self.logger.log(level, msg)
+
+ def close(self):
+ self.active = 0
+ self.engine_in = self.engine_out = None
+ self.sequence_number_in = self.sequence_number_out = 0L
+ for chan in self.channels.values():
+ chan.unlink()
+
+ def get_host_key(self):
+ 'returns (type, key) where type is like "ssh-rsa" and key is an opaque string'
+ if (not self.active) or (not self.initial_kex_done):
+ raise SSHException('No existing session')
+ key_msg = Message(self.host_key)
+ key_type = key_msg.get_string()
+ return key_type, self.host_key
+
+ def is_active(self):
+ return self.active
+
+ def is_authenticated(self):
+ return self.authenticated and self.active
+
+ def open_session(self):
+ return self.open_channel('session')
+
+ def open_channel(self, kind):
+ chan = None
+ try:
+ self.lock.acquire()
+ chanid = self.channel_counter
+ self.channel_counter += 1
+ m = Message()
+ m.add_byte(chr(MSG_CHANNEL_OPEN))
+ m.add_string(kind)
+ m.add_int(chanid)
+ m.add_int(self.window_size)
+ m.add_int(self.max_packet_size)
+ self.channels[chanid] = chan = Channel(chanid, self)
+ self.channel_events[chanid] = event = threading.Event()
+ chan.set_window(self.window_size, self.max_packet_size)
+ self.send_message(m)
+ finally:
+ self.lock.release()
+ while 1:
+ event.wait(0.1);
+ if not self.active:
+ return None
+ if event.isSet():
+ break
+ try:
+ self.lock.acquire()
+ if not self.channels.has_key(chanid):
+ chan = None
+ finally:
+ self.lock.release()
+ return chan
+
+ def unlink_channel(self, chanid):
+ try:
+ self.lock.acquire()
+ if self.channels.has_key(chanid):
+ del self.channels[chanid]
+ finally:
+ self.lock.release()
+
+ def read_all(self, n):
+ out = ''
+ while n > 0:
+ try:
+ x = self.sock.recv(n)
+ if len(x) == 0:
+ raise EOFError()
+ out += x
+ n -= len(x)
+ except socket.timeout:
+ if not self.active:
+ raise EOFError()
+ return out
+
+ def write_all(self, out):
+ while len(out) > 0:
+ n = self.sock.send(out)
+ if n <= 0:
+ raise EOFError()
+ if n == len(out):
+ return
+ out = out[n:]
+ return
+
+ def build_packet(self, payload):
+ # pad up at least 4 bytes, to nearest block-size (usually 8)
+ bsize = self.block_size_out
+ padding = 3 + bsize - ((len(payload) + 8) % bsize)
+ packet = struct.pack('>I', len(payload) + padding + 1)
+ packet += chr(padding)
+ packet += payload
+ packet += randpool.get_bytes(padding)
+ return packet
+
+ def send_message(self, data):
+ # encrypt this sucka
+ packet = self.build_packet(str(data))
+ if self.ultra_debug:
+ self.log(DEBUG, format_binary(packet, 'OUT: '))
+ if self.engine_out != None:
+ out = self.engine_out.encrypt(packet)
+ else:
+ out = packet
+ # + mac
+ try:
+ self.write_lock.acquire()
+ if self.engine_out != None:
+ payload = struct.pack('>I', self.sequence_number_out) + packet
+ out += HMAC.HMAC(self.mac_key_out, payload, self.local_mac_engine).digest()[:self.local_mac_len]
+ self.sequence_number_out += 1L
+ self.sequence_number_out %= 0x100000000L
+ self.write_all(out)
+ finally:
+ self.write_lock.release()
+
+ def read_message(self):
+ "only one thread will ever be in this function"
+ header = self.read_all(self.block_size_in)
+ if self.engine_in != None:
+ header = self.engine_in.decrypt(header)
+ if self.ultra_debug:
+ self.log(DEBUG, format_binary(header, 'IN: '));
+ packet_size = struct.unpack('>I', header[:4])[0]
+ # leftover contains decrypted bytes from the first block (after the length field)
+ leftover = header[4:]
+ if (packet_size - len(leftover)) % self.block_size_in != 0:
+ raise SSHException('Invalid packet blocking')
+ buffer = self.read_all(packet_size + self.remote_mac_len - len(leftover))
+ packet = buffer[:packet_size - len(leftover)]
+ post_packet = buffer[packet_size - len(leftover):]
+ if self.engine_in != None:
+ packet = self.engine_in.decrypt(packet)
+ if self.ultra_debug:
+ self.log(DEBUG, format_binary(packet, 'IN: '));
+ packet = leftover + packet
+ if self.remote_mac_len > 0:
+ mac = post_packet[:self.remote_mac_len]
+ mac_payload = struct.pack('>II', self.sequence_number_in, packet_size) + packet
+ my_mac = HMAC.HMAC(self.mac_key_in, mac_payload, self.remote_mac_engine).digest()[:self.remote_mac_len]
+ if my_mac != mac:
+ raise SSHException('Mismatched MAC')
+ padding = ord(packet[0])
+ payload = packet[1:packet_size - padding + 1]
+ randpool.add_event(packet[packet_size - padding + 1])
+ #self.log(DEBUG, 'Got payload (%d bytes, %d padding)' % (packet_size, padding))
+ msg = Message(payload[1:])
+ msg.seqno = self.sequence_number_in
+ self.sequence_number_in = (self.sequence_number_in + 1) & 0xffffffffL
+ # check for rekey
+ self.received_bytes += packet_size + self.remote_mac_len + 4
+ self.received_packets += 1
+ if (self.received_packets >= self.REKEY_PACKETS) or (self.received_bytes >= self.REKEY_BYTES):
+ # only ask once for rekeying
+ if self.local_kex_init is None:
+ self.log(DEBUG, 'Rekeying (hit %d packets, %d bytes)' % (self.received_packets,
+ self.received_bytes))
+ self.received_packets_overflow = 0
+ self.send_kex_init()
+ else:
+ # we've asked to rekey already -- give them 20 packets to
+ # comply, then just drop the connection
+ self.received_packets_overflow += 1
+ if self.received_packets_overflow >= 20:
+ raise SSHException('Remote transport is ignoring rekey requests')
+
+ return ord(payload[0]), msg
+
+ def set_K_H(self, k, h):
+ "used by a kex object to set the K (root key) and H (exchange hash)"
+ self.K = k
+ self.H = h
+ if self.session_id == None:
+ self.session_id = h
+
+ def verify_key(self, host_key, sig):
+ if self.host_key_type == 'ssh-rsa':
+ key = RSAKey(Message(host_key))
+ elif self.host_key_type == 'ssh-dss':
+ key = DSSKey(Message(host_key))
+ else:
+ key = None
+ if (key == None) or not key.valid:
+ raise SSHException('Unknown host key type')
+ if not key.verify_ssh_sig(self.H, Message(sig)):
+ raise SSHException('Signature verification (%s) failed. Boo. Robey should debug this.' % self.host_key_type)
+ self.host_key = host_key
+
+ def compute_key(self, id, nbytes):
+ "id is 'A' - 'F' for the various keys used by ssh"
+ m = Message()
+ m.add_mpint(self.K)
+ m.add_bytes(self.H)
+ m.add_byte(id)
+ m.add_bytes(self.session_id)
+ out = sofar = SHA.new(str(m)).digest()
+ while len(out) < nbytes:
+ m = Message()
+ m.add_mpint(self.K)
+ m.add_bytes(self.H)
+ m.add_bytes(sofar)
+ hash = SHA.new(str(m)).digest()
+ out += hash
+ sofar += hash
+ return out[:nbytes]
+
+ def get_cipher(self, name, key, iv):
+ if not self.cipher_info.has_key(name):
+ raise SSHException('Unknown client cipher ' + name)
+ return self.cipher_info[name]['class'].new(key, self.cipher_info[name]['mode'], iv)
+
+ def run(self):
+ self.active = 1
+ try:
+ # SSH-1.99-OpenSSH_2.9p2
+ self.write_all(self.local_version + '\r\n')
+ self.check_banner()
+ self.send_kex_init()
+ self.expected_packet = MSG_KEXINIT
+
+ while self.active:
+ ptype, m = self.read_message()
+ if ptype == MSG_IGNORE:
+ continue
+ elif ptype == MSG_DISCONNECT:
+ self.parse_disconnect(m)
+ self.active = 0
+ break
+ elif ptype == MSG_DEBUG:
+ self.parse_debug(m)
+ continue
+ if self.expected_packet != 0:
+ if ptype != self.expected_packet:
+ raise SSHException('Expecting packet %d, got %d' % (self.expected_packet, ptype))
+ self.expected_packet = 0
+ if (ptype >= 30) and (ptype <= 39):
+ self.kex_engine.parse_next(ptype, m)
+ continue
+
+ if self.handler_table.has_key(ptype):
+ self.handler_table[ptype](self, m)
+ elif self.channel_handler_table.has_key(ptype):
+ chanid = m.get_int()
+ if self.channels.has_key(chanid):
+ self.channel_handler_table[ptype](self.channels[chanid], m)
+ else:
+ self.log(WARNING, 'Oops, unhandled type %d' % ptype)
+ msg = Message()
+ msg.add_byte(chr(MSG_UNIMPLEMENTED))
+ msg.add_int(m.seqno)
+ self.send_message(msg)
+ except SSHException, e:
+ self.log(DEBUG, 'Exception: ' + str(e))
+ except EOFError, e:
+ self.log(DEBUG, 'EOF')
+ except Exception, e:
+ self.log(DEBUG, 'Unknown exception: ' + str(e))
+ if self.active:
+ self.active = 0
+ if self.completion_event != None:
+ self.completion_event.set()
+ if self.auth_event != None:
+ self.auth_event.set()
+ for e in self.channel_events.values():
+ e.set()
+ self.sock.close()
+
+ ### protocol stages
+
+ def renegotiate_keys(self):
+ self.completion_event = threading.Event()
+ self.send_kex_init()
+ while 1:
+ self.completion_event.wait(0.1);
+ if not self.active:
+ return 0
+ if self.completion_event.isSet():
+ break
+ return 1
+
+ def negotiate_keys(self, m):
+ # throws SSHException on anything unusual
+ if self.local_kex_init == None:
+ # remote side wants to renegotiate
+ self.send_kex_init()
+ self.parse_kex_init(m)
+ self.kex_engine.start_kex()
+
+ def check_banner(self):
+ # this is slow, but we only have to do it once
+ for i in range(5):
+ buffer = ''
+ while not '\n' in buffer:
+ buffer += self.read_all(1)
+ buffer = buffer[:-1]
+ if (len(buffer) > 0) and (buffer[-1] == '\r'):
+ buffer = buffer[:-1]
+ if buffer[:4] == 'SSH-':
+ break
+ self.log(DEBUG, 'Banner: ' + buffer)
+ if buffer[:4] != 'SSH-':
+ raise SSHException('Indecipherable protocol version "' + buffer + '"')
+ # save this server version string for later
+ self.remote_version = buffer
+ # pull off any attached comment
+ comment = ''
+ i = string.find(buffer, ' ')
+ if i >= 0:
+ comment = buffer[i+1:]
+ buffer = buffer[:i]
+ # parse out version string and make sure it matches
+ _unused, version, client = string.split(buffer, '-')
+ if version != '1.99' and version != '2.0':
+ raise SSHException('Incompatible version (%s instead of 2.0)' % (version,))
+ self.log(INFO, 'Connected (version %s, client %s)' % (version, client))
+
+ def send_kex_init(self):
+ # send a really wimpy kex-init packet that says we're a bare-bones ssh client
+ m = Message()
+ m.add_byte(chr(MSG_KEXINIT))
+ m.add_bytes(randpool.get_bytes(16))
+ m.add(','.join(self.preferred_kex))
+ m.add(','.join(self.preferred_keys))
+ m.add(','.join(self.preferred_ciphers))
+ m.add(','.join(self.preferred_ciphers))
+ m.add(','.join(self.preferred_macs))
+ m.add(','.join(self.preferred_macs))
+ m.add('none')
+ m.add('none')
+ m.add('')
+ m.add('')
+ m.add_boolean(0)
+ m.add_int(0)
+ # save a copy for later (needed to compute a hash)
+ self.local_kex_init = str(m)
+ self.send_message(m)
+
+ def parse_kex_init(self, m):
+ # reset counters of when to re-key, since we are now re-keying
+ self.received_bytes = 0
+ self.received_packets = 0
+ self.received_packets_overflow = 0
+
+ cookie = m.get_bytes(16)
+ kex_algo_list = m.get_list()
+ server_key_algo_list = m.get_list()
+ client_encrypt_algo_list = m.get_list()
+ server_encrypt_algo_list = m.get_list()
+ client_mac_algo_list = m.get_list()
+ server_mac_algo_list = m.get_list()
+ client_compress_algo_list = m.get_list()
+ server_compress_algo_list = m.get_list()
+ client_lang_list = m.get_list()
+ server_lang_list = m.get_list()
+ kex_follows = m.get_boolean()
+ unused = m.get_int()
+
+ # no compression support (yet?)
+ if (not('none' in client_compress_algo_list) or
+ not('none' in server_compress_algo_list)):
+ raise SSHException('Incompatible ssh peer.')
+
+ # as a server, we pick the first item in the client's list that we support.
+ # as a client, we pick the first item in our list that the server supports.
+ if self.server_mode:
+ agreed_kex = filter(self.preferred_kex.__contains__, kex_algo_list)
+ else:
+ agreed_kex = filter(kex_algo_list.__contains__, self.preferred_kex)
+ if len(agreed_kex) == 0:
+ raise SSHException('Incompatible ssh peer (no acceptable kex algorithm)')
+ self.kex_engine = self.kex_info[agreed_kex[0]](self)
+
+ if self.server_mode:
+ agreed_keys = filter(self.preferred_keys.__contains__, server_key_algo_list)
+ else:
+ agreed_keys = filter(server_key_algo_list.__contains__, self.preferred_keys)
+ if len(agreed_keys) == 0:
+ raise SSHException('Incompatible ssh peer (no acceptable host key)')
+ self.host_key_type = agreed_keys[0]
+ if self.server_mode and (self.get_server_key() is None):
+ raise SSHException('Incompatible ssh peer (can\'t match requested host key type)')
+
+ if self.server_mode:
+ agreed_local_ciphers = filter(self.preferred_ciphers.__contains__,
+ server_encrypt_algo_list)
+ agreed_remote_ciphers = filter(self.preferred_ciphers.__contains__,
+ client_encrypt_algo_list)
+ else:
+ agreed_local_ciphers = filter(client_encrypt_algo_list.__contains__,
+ self.preferred_ciphers)
+ agreed_remote_ciphers = filter(server_encrypt_algo_list.__contains__,
+ self.preferred_ciphers)
+ if (len(agreed_local_ciphers) == 0) or (len(agreed_remote_ciphers) == 0):
+ raise SSHException('Incompatible ssh server (no acceptable ciphers)')
+ self.local_cipher = agreed_local_ciphers[0]
+ self.remote_cipher = agreed_remote_ciphers[0]
+ self.log(DEBUG, 'Ciphers agreed: local=%s, remote=%s' % (self.local_cipher, self.remote_cipher))
+
+ if self.server_mode:
+ agreed_remote_macs = filter(self.preferred_macs.__contains__, client_mac_algo_list)
+ agreed_local_macs = filter(self.preferred_macs.__contains__, server_mac_algo_list)
+ else:
+ agreed_local_macs = filter(client_mac_algo_list.__contains__, self.preferred_macs)
+ agreed_remote_macs = filter(server_mac_algo_list.__contains__, self.preferred_macs)
+ if (len(agreed_local_macs) == 0) or (len(agreed_remote_macs) == 0):
+ raise SSHException('Incompatible ssh server (no acceptable macs)')
+ self.local_mac = agreed_local_macs[0]
+ self.remote_mac = agreed_remote_macs[0]
+
+ self.log(DEBUG, 'kex algos:' + str(kex_algo_list) + ' server key:' + str(server_key_algo_list) + \
+ ' client encrypt:' + str(client_encrypt_algo_list) + \
+ ' server encrypt:' + str(server_encrypt_algo_list) + \
+ ' client mac:' + str(client_mac_algo_list) + \
+ ' server mac:' + str(server_mac_algo_list) + \
+ ' client compress:' + str(client_compress_algo_list) + \
+ ' server compress:' + str(server_compress_algo_list) + \
+ ' client lang:' + str(client_lang_list) + \
+ ' server lang:' + str(server_lang_list) + \
+ ' kex follows?' + str(kex_follows))
+ self.log(DEBUG, 'using kex %s; server key type %s; cipher: local %s, remote %s; mac: local %s, remote %s' %
+ (agreed_kex[0], self.host_key_type, self.local_cipher, self.remote_cipher, self.local_mac,
+ self.remote_mac))
+
+ # save for computing hash later...
+ # now wait! openssh has a bug (and others might too) where there are
+ # actually some extra bytes (one NUL byte in openssh's case) added to
+ # the end of the packet but not parsed. turns out we need to throw
+ # away those bytes because they aren't part of the hash.
+ self.remote_kex_init = chr(MSG_KEXINIT) + m.get_so_far()
+
+ def activate_inbound(self):
+ "switch on newly negotiated encryption parameters for inbound traffic"
+ self.block_size_in = self.cipher_info[self.remote_cipher]['block-size']
+ if self.server_mode:
+ IV_in = self.compute_key('A', self.block_size_in)
+ key_in = self.compute_key('C', self.cipher_info[self.remote_cipher]['key-size'])
+ else:
+ IV_in = self.compute_key('B', self.block_size_in)
+ key_in = self.compute_key('D', self.cipher_info[self.remote_cipher]['key-size'])
+ self.engine_in = self.get_cipher(self.remote_cipher, key_in, IV_in)
+ self.remote_mac_len = self.mac_info[self.remote_mac]['size']
+ self.remote_mac_engine = self.mac_info[self.remote_mac]['class']
+ # initial mac keys are done in the hash's natural size (not the potentially truncated
+ # transmission size)
+ if self.server_mode:
+ self.mac_key_in = self.compute_key('E', self.remote_mac_engine.digest_size)
+ else:
+ self.mac_key_in = self.compute_key('F', self.remote_mac_engine.digest_size)
+
+ def activate_outbound(self):
+ "switch on newly negotiated encryption parameters for outbound traffic"
+ m = Message()
+ m.add_byte(chr(MSG_NEWKEYS))
+ self.send_message(m)
+ self.block_size_out = self.cipher_info[self.local_cipher]['block-size']
+ if self.server_mode:
+ IV_out = self.compute_key('B', self.block_size_out)
+ key_out = self.compute_key('D', self.cipher_info[self.local_cipher]['key-size'])
+ else:
+ IV_out = self.compute_key('A', self.block_size_out)
+ key_out = self.compute_key('C', self.cipher_info[self.local_cipher]['key-size'])
+ self.engine_out = self.get_cipher(self.local_cipher, key_out, IV_out)
+ self.local_mac_len = self.mac_info[self.local_mac]['size']
+ self.local_mac_engine = self.mac_info[self.local_mac]['class']
+ # initial mac keys are done in the hash's natural size (not the potentially truncated
+ # transmission size)
+ if self.server_mode:
+ self.mac_key_out = self.compute_key('F', self.local_mac_engine.digest_size)
+ else:
+ self.mac_key_out = self.compute_key('E', self.local_mac_engine.digest_size)
+
+ def parse_newkeys(self, m):
+ self.log(DEBUG, 'Switch to new keys ...')
+ self.activate_inbound()
+ # can also free a bunch of stuff here
+ self.local_kex_init = self.remote_kex_init = None
+ self.e = self.f = self.K = self.x = None
+ if not self.initial_kex_done:
+ # this was the first key exchange
+ self.initial_kex_done = 1
+ # send an event?
+ if self.completion_event != None:
+ self.completion_event.set()
+ return
+
+ def parse_disconnect(self, m):
+ code = m.get_int()
+ desc = m.get_string()
+ self.log(INFO, 'Disconnect (code %d): %s' % (code, desc))
+ def parse_channel_open_success(self, m):
+ chanid = m.get_int()
+ server_chanid = m.get_int()
+ server_window_size = m.get_int()
+ server_max_packet_size = m.get_int()
+ if not self.channels.has_key(chanid):
+ self.log(WARNING, 'Success for unrequested channel! [??]')
+ return
+ try:
+ self.lock.acquire()
+ chan = self.channels[chanid]
+ chan.set_server_channel(server_chanid, server_window_size, server_max_packet_size)
+ self.log(INFO, 'Secsh channel %d opened.' % chanid)
+ if self.channel_events.has_key(chanid):
+ self.channel_events[chanid].set()
+ del self.channel_events[chanid]
+ finally:
+ self.lock.release()
+ return
+
+ def parse_channel_open_failure(self, m):
+ chanid = m.get_int()
+ reason = m.get_int()
+ reason_str = m.get_string()
+ lang = m.get_string()
+ if CONNECTION_FAILED_CODE.has_key(reason):
+ reason_text = CONNECTION_FAILED_CODE[reason]
+ else:
+ reason_text = '(unknown code)'
+ self.log(INFO, 'Secsh channel %d open FAILED: %s: %s' % (chanid, reason_str, reason_text))
+ try:
+ self.lock.aquire()
+ if self.channels.has_key(chanid):
+ del self.channels[chanid]
+ if self.channel_events.has_key(chanid):
+ self.channel_events[chanid].set()
+ del self.channel_events[chanid]
+ finally:
+ self.lock_release()
+ return
+
+ def parse_channel_open(self, m):
+ kind = m.get_string()
+ self.log(DEBUG, 'Rejecting "%s" channel request from server.' % kind)
+ chanid = m.get_int()
+ msg = Message()
+ msg.add_byte(chr(MSG_CHANNEL_OPEN_FAILURE))
+ msg.add_int(chanid)
+ msg.add_int(1)
+ msg.add_string('Client connections are not allowed.')
+ msg.add_string('en')
+ self.send_message(msg)
+
+ def parse_debug(self, m):
+ always_display = m.get_boolean()
+ msg = m.get_string()
+ lang = m.get_string()
+ self.log(DEBUG, 'Debug msg: ' + safe_string(msg))
+
+ handler_table = {
+ MSG_NEWKEYS: parse_newkeys,
+ MSG_CHANNEL_OPEN_SUCCESS: parse_channel_open_success,
+ MSG_CHANNEL_OPEN_FAILURE: parse_channel_open_failure,
+ MSG_CHANNEL_OPEN: parse_channel_open,
+ MSG_KEXINIT: negotiate_keys,
+ }
+
+ channel_handler_table = {
+ MSG_CHANNEL_SUCCESS: Channel.request_success,
+ MSG_CHANNEL_FAILURE: Channel.request_failed,
+ MSG_CHANNEL_DATA: Channel.feed,
+ MSG_CHANNEL_WINDOW_ADJUST: Channel.window_adjust,
+ MSG_CHANNEL_REQUEST: Channel.handle_request,
+ MSG_CHANNEL_EOF: Channel.handle_eof,
+ MSG_CHANNEL_CLOSE: Channel.handle_close,
+ }
diff --git a/util.py b/util.py
new file mode 100644
index 00000000..28349721
--- /dev/null
+++ b/util.py
@@ -0,0 +1,89 @@
+#!/usr/bin/python
+
+import struct
+
+def inflate_long(s, always_positive=0):
+ "turns a normalized byte string into a long-int (adapted from Crypto.Util.number)"
+ out = 0L
+ negative = 0
+ if not always_positive and (len(s) > 0) and (ord(s[0]) >= 0x80):
+ negative = 1
+ if len(s) % 4:
+ filler = '\x00'
+ if negative:
+ filler = '\xff'
+ s = filler * (4 - len(s) % 4) + s
+ for i in range(0, len(s), 4):
+ out = (out << 32) + struct.unpack('>I', s[i:i+4])[0]
+ if negative:
+ out -= (1L << (8 * len(s)))
+ 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) and (n != -1):
+ s = struct.pack('>I', n & 0xffffffffL) + s
+ n = n >> 32
+ # strip off leading zeros, FFs
+ for i in enumerate(s):
+ if (n == 0) and (i[1] != '\000'):
+ break
+ if (n == -1) and (i[1] != '\xff'):
+ break
+ else:
+ # degenerate case, n was either 0 or -1
+ i = (0,)
+ if n == 0:
+ s = '\000'
+ else:
+ s = '\xff'
+ s = s[i[0]:]
+ if add_sign_padding:
+ if (n == 0) and (ord(s[0]) >= 0x80):
+ s = '\x00' + s
+ if (n == -1) and (ord(s[0]) < 0x80):
+ s = '\xff' + s
+ return s
+
+def format_binary_weird(data):
+ out = ''
+ for i in enumerate(data):
+ out += '%02X' % ord(i[1])
+ if i[0] % 2:
+ out += ' '
+ if i[0] % 16 == 15:
+ out += '\n'
+ return out
+
+def format_binary(data, prefix=''):
+ x = 0
+ out = []
+ while len(data) > x + 16:
+ out.append(format_binary_line(data[x:x+16]))
+ x += 16
+ if x < len(data):
+ out.append(format_binary_line(data[x:]))
+ return [prefix + x for x in out]
+
+def format_binary_line(data):
+ left = ' '.join(['%02X' % ord(c) for c in data])
+ right = ''.join([('.%c..' % c)[(ord(c)+61)//94] for c in data])
+ return '%-50s %s' % (left, right)
+
+def hexify(s):
+ "turn a string into a hex sequence"
+ return ''.join(['%02X' % ord(c) for c in s])
+
+def safe_string(s):
+ out = ''
+ for c in s:
+ if (ord(c) >= 32) and (ord(c) <= 127):
+ out += c
+ else:
+ out += '%%%02X' % ord(c)
+ return out
+
+# ''.join([['%%%02X' % ord(c), c][(ord(c) >= 32) and (ord(c) <= 127)] for c in s])