diff --git a/.rustfmt.toml b/.rustfmt.toml
new file mode 100644
index 0000000000000000000000000000000000000000..a58ec5754d6a3aabcc71e7d07a284c03a8e072c7
--- /dev/null
+++ b/.rustfmt.toml
@@ -0,0 +1,6 @@
+max_width = 85
+use_small_heuristics = "Max"
+fn_args_layout = "Compressed"
+# unstable
+# trailing_semicolon = false
+# overflow_delimited_expr  = true
diff --git a/Cargo.lock b/Cargo.lock
index 97fcfdf2e060c7fd30412a9dc791010078a6649b..1788f81db79e1613c713527fad10c85a800fb7bc 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2,12 +2,130 @@
 # It is not intended for manual editing.
 version = 3
 
+[[package]]
+name = "aes"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfe0133578c0986e1fe3dfcd4af1cc5b2dd6c3dbf534d69916ce16a2701d40ba"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures",
+]
+
 [[package]]
 name = "anyhow"
 version = "1.0.56"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27"
 
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899"
+
+[[package]]
+name = "bytes"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
+
+[[package]]
+name = "cc"
+version = "1.0.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "chrono"
+version = "0.4.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
+dependencies = [
+ "libc",
+ "num-integer",
+ "num-traits",
+ "time",
+ "winapi",
+]
+
+[[package]]
+name = "cipher"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1873270f8f7942c191139cb8a40fd228da6c3fd2fc376d7e92d47aa14aeb59e"
+dependencies = [
+ "crypto-common",
+ "inout",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "ctr"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d14f329cfbaf5d0e06b5e87fff7e265d2673c5ea7d2c27691a2c107db1442a0"
+dependencies = [
+ "cipher",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+ "subtle",
+]
+
 [[package]]
 name = "doc-comment"
 version = "0.3.3"
@@ -22,10 +140,51 @@ version = "0.1.0"
 name = "door-sshproto"
 version = "0.1.0"
 dependencies = [
+ "aes",
  "anyhow",
+ "ctr",
+ "hmac",
+ "log",
+ "pretty-hex",
+ "rand",
+ "ring",
  "serde",
+ "sha2",
+ "snafu",
+]
+
+[[package]]
+name = "door-tokio"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "door-sshproto",
+ "log",
+ "pretty-hex",
+ "simplelog",
  "snafu",
- "toml",
+ "tokio",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.10.2+wasi-snapshot-preview1",
 ]
 
 [[package]]
@@ -37,6 +196,181 @@ dependencies = [
  "unicode-segmentation",
 ]
 
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "hmac"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+dependencies = [
+ "digest",
+]
+
+[[package]]
+name = "inout"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.57"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "671a26f820db17c2a2750743f1dd03bafd15b98c9f30c7c2628c024c05d73397"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libc"
+version = "0.2.123"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb691a747a7ab48abc15c5b42066eaafde10dc427e3b6ee2a1cf43db04c763bd"
+
+[[package]]
+name = "lock_api"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "memchr"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
+
+[[package]]
+name = "mio"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52da4364ffb0e4fe33a9841a98a3f3014fb964045ce4f7a45a398243c8d6b0c9"
+dependencies = [
+ "libc",
+ "log",
+ "miow",
+ "ntapi",
+ "wasi 0.11.0+wasi-snapshot-preview1",
+ "winapi",
+]
+
+[[package]]
+name = "miow"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "ntapi"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
+dependencies = [
+ "autocfg",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "995f667a6c822200b0433ac218e05582f0e2efa1b922a3fd2fbaadc5f87bab37"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-sys",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c"
+
+[[package]]
+name = "pretty-hex"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc5c99d529f0d30937f6f4b8a86d988047327bb88d04d2c4afc356de74722131"
+
 [[package]]
 name = "proc-macro2"
 version = "1.0.37"
@@ -55,6 +389,54 @@ dependencies = [
  "proc-macro2",
 ]
 
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "ring"
+version = "0.16.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
+dependencies = [
+ "cc",
+ "libc",
+ "once_cell",
+ "spin",
+ "untrusted",
+ "web-sys",
+ "winapi",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
+
 [[package]]
 name = "serde"
 version = "1.0.136"
@@ -75,6 +457,43 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "sha2"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "simplelog"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1348164456f72ca0116e4538bdaabb0ddb622c7d9f16387c725af3e96d6001c"
+dependencies = [
+ "chrono",
+ "log",
+ "termcolor",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83"
+
 [[package]]
 name = "snafu"
 version = "0.7.0"
@@ -97,6 +516,28 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "socket2"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "spin"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
+
+[[package]]
+name = "subtle"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
+
 [[package]]
 name = "syn"
 version = "1.0.91"
@@ -109,14 +550,61 @@ dependencies = [
 ]
 
 [[package]]
-name = "toml"
-version = "0.5.8"
+name = "termcolor"
+version = "1.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa"
+checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
 dependencies = [
- "serde",
+ "winapi-util",
+]
+
+[[package]]
+name = "time"
+version = "0.1.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "tokio"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2af73ac49756f3f7c01172e34a23e5d0216f6c32333757c2c61feb2bbff5a5ee"
+dependencies = [
+ "bytes",
+ "libc",
+ "memchr",
+ "mio",
+ "num_cpus",
+ "once_cell",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2",
+ "tokio-macros",
+ "winapi",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
 ]
 
+[[package]]
+name = "typenum"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
+
 [[package]]
 name = "unicode-segmentation"
 version = "1.9.0"
@@ -128,3 +616,165 @@ name = "unicode-xid"
 version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
+
+[[package]]
+name = "untrusted"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "wasi"
+version = "0.10.2+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.80"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.80"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53e04185bfa3a779273da532f5025e33398409573f348985af9a1cbf3774d3f4"
+dependencies = [
+ "bumpalo",
+ "lazy_static",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.80"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.80"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.80"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744"
+
+[[package]]
+name = "web-sys"
+version = "0.3.57"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b17e741662c70c8bd24ac5c5b18de314a2c26c32bf8346ee1e6f53de919c283"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-sys"
+version = "0.34.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5acdd78cb4ba54c0045ac14f62d8f94a03d10047904ae2a40afa1e99d8f70825"
+dependencies = [
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.34.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17cffbe740121affb56fad0fc0e421804adf0ae00891205213b5cecd30db881d"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.34.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2564fde759adb79129d9b4f54be42b32c89970c18ebf93124ca8870a498688ed"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.34.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9cd9d32ba70453522332c14d38814bceeb747d80b3958676007acadd7e166956"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.34.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfce6deae227ee8d356d19effc141a509cc503dfd1f850622ec4b0f84428e1f4"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.34.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9"
diff --git a/Cargo.toml b/Cargo.toml
index 84253fe120f48d6f3e64185be3e954f0cd31288a..cf6fe077f1362943aa11c5efa51493110cc41444 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -2,10 +2,21 @@
 name = "door"
 version = "0.1.0"
 edition = "2021"
+license = "MPL-2.0"
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [workspace]
-members = ["door-sshproto"]
+members = [
+    "door-sshproto",
+    "door-tokio",
+]
 
-[dependencies]
+[profile.release]
+opt-level = 's'
+lto = "fat"
+debug = 1
+
+[patch.crates-io]
+# serde_state = { version = "0.4", path = "../../3rd/serde_state/serde_state" }
+# serde_derive_state = { version = "0.4", path = "../../3rd/serde_state/serde_derive" }
diff --git a/door-sshproto/Cargo.toml b/door-sshproto/Cargo.toml
index e9f31e73b43933b166cc920520b9701782bf6635..0c409a47c5bcc9bc253e609538c5d4753720c063 100644
--- a/door-sshproto/Cargo.toml
+++ b/door-sshproto/Cargo.toml
@@ -3,19 +3,36 @@ name = "door-sshproto"
 version = "0.1.0"
 edition = "2021"
 
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
-
 [dependencies]
-# snafu = { version = "0.7", default-features = false }
-snafu = { version = "0.7" }
-# nom = { version = "7.1", default-features = false }
 serde = { version = "1.0", default-features = false, features = ["derive"]}
+snafu = { version = "0.7", default-features = false }
+# TODO: check that log macro calls disappear in no_std builds
+log = { version = "0.4" }
+# TODO: probably needs changing for embedded platforms
+rand = { version = "0.8", default-features = false, features = ["getrandom"] }
+
+ring = { version = "0.16", default-features = false }
+# for aes-ctr mode
+ctr = "0.9"
+aes = "0.8"
+sha2 = "0.10"
+hmac = "0.12"
+
+pretty-hex = "0.2"
+
+[features]
+std = []
+
+# serde_state = { version = "0.4", default-features = false, features = ["derive"]}
+# serde_derive_state = { version = "0.4" }
 
 [dev-dependencies]
-toml = "0.5"
+# toml = "0.5"
 # examples want std::error
 snafu = { version = "0.7", default-features = true }
 anyhow = { version = "1.0" }
+pretty-hex = "0.2"
 
-
+[[example]]
+name = "kex1"
 
diff --git a/door-sshproto/LICENSE b/door-sshproto/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..f212b3d46a17aa0ef0c10a82f924c33856d77e26
--- /dev/null
+++ b/door-sshproto/LICENSE
@@ -0,0 +1,377 @@
+Door SSH is (c) 2022 Matt Johnston and contributors
+Provided under terms of the MPL 2.0 as below.
+
+
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+    means each individual or legal entity that creates, contributes to
+    the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+    means the combination of the Contributions of others (if any) used
+    by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+    means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+    means Source Code Form to which the initial Contributor has attached
+    the notice in Exhibit A, the Executable Form of such Source Code
+    Form, and Modifications of such Source Code Form, in each case
+    including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+    means
+
+    (a) that the initial Contributor has attached the notice described
+        in Exhibit B to the Covered Software; or
+
+    (b) that the Covered Software was made available under the terms of
+        version 1.1 or earlier of the License, but not also under the
+        terms of a Secondary License.
+
+1.6. "Executable Form"
+    means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+    means a work that combines Covered Software with other material, in 
+    a separate file or files, that is not Covered Software.
+
+1.8. "License"
+    means this document.
+
+1.9. "Licensable"
+    means having the right to grant, to the maximum extent possible,
+    whether at the time of the initial grant or subsequently, any and
+    all of the rights conveyed by this License.
+
+1.10. "Modifications"
+    means any of the following:
+
+    (a) any file in Source Code Form that results from an addition to,
+        deletion from, or modification of the contents of Covered
+        Software; or
+
+    (b) any new file in Source Code Form that contains any Covered
+        Software.
+
+1.11. "Patent Claims" of a Contributor
+    means any patent claim(s), including without limitation, method,
+    process, and apparatus claims, in any patent Licensable by such
+    Contributor that would be infringed, but for the grant of the
+    License, by the making, using, selling, offering for sale, having
+    made, import, or transfer of either its Contributions or its
+    Contributor Version.
+
+1.12. "Secondary License"
+    means either the GNU General Public License, Version 2.0, the GNU
+    Lesser General Public License, Version 2.1, the GNU Affero General
+    Public License, Version 3.0, or any later versions of those
+    licenses.
+
+1.13. "Source Code Form"
+    means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+    means an individual or a legal entity exercising rights under this
+    License. For legal entities, "You" includes any entity that
+    controls, is controlled by, or is under common control with You. For
+    purposes of this definition, "control" means (a) the power, direct
+    or indirect, to cause the direction or management of such entity,
+    whether by contract or otherwise, or (b) ownership of more than
+    fifty percent (50%) of the outstanding shares or beneficial
+    ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+    Licensable by such Contributor to use, reproduce, make available,
+    modify, display, perform, distribute, and otherwise exploit its
+    Contributions, either on an unmodified basis, with Modifications, or
+    as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+    for sale, have made, import, and otherwise transfer either its
+    Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+    or
+
+(b) for infringements caused by: (i) Your and any other third party's
+    modifications of Covered Software, or (ii) the combination of its
+    Contributions with other software (except as part of its Contributor
+    Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+    its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+    Form, as described in Section 3.1, and You must inform recipients of
+    the Executable Form how they can obtain a copy of such Source Code
+    Form by reasonable means in a timely manner, at a charge no more
+    than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+    License, or sublicense it under different terms, provided that the
+    license for the Executable Form does not attempt to limit or alter
+    the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+*                                                                      *
+*  6. Disclaimer of Warranty                                           *
+*  -------------------------                                           *
+*                                                                      *
+*  Covered Software is provided under this License on an "as is"       *
+*  basis, without warranty of any kind, either expressed, implied, or  *
+*  statutory, including, without limitation, warranties that the       *
+*  Covered Software is free of defects, merchantable, fit for a        *
+*  particular purpose or non-infringing. The entire risk as to the     *
+*  quality and performance of the Covered Software is with You.        *
+*  Should any Covered Software prove defective in any respect, You     *
+*  (not any Contributor) assume the cost of any necessary servicing,   *
+*  repair, or correction. This disclaimer of warranty constitutes an   *
+*  essential part of this License. No use of any Covered Software is   *
+*  authorized under this License except under this disclaimer.         *
+*                                                                      *
+************************************************************************
+
+************************************************************************
+*                                                                      *
+*  7. Limitation of Liability                                          *
+*  --------------------------                                          *
+*                                                                      *
+*  Under no circumstances and under no legal theory, whether tort      *
+*  (including negligence), contract, or otherwise, shall any           *
+*  Contributor, or anyone who distributes Covered Software as          *
+*  permitted above, be liable to You for any direct, indirect,         *
+*  special, incidental, or consequential damages of any character      *
+*  including, without limitation, damages for lost profits, loss of    *
+*  goodwill, work stoppage, computer failure or malfunction, or any    *
+*  and all other commercial damages or losses, even if such party      *
+*  shall have been informed of the possibility of such damages. This   *
+*  limitation of liability shall not apply to liability for death or   *
+*  personal injury resulting from such party's negligence to the       *
+*  extent applicable law prohibits such limitation. Some               *
+*  jurisdictions do not allow the exclusion or limitation of           *
+*  incidental or consequential damages, so this exclusion and          *
+*  limitation may not apply to You.                                    *
+*                                                                      *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+  This Source Code Form is subject to the terms of the Mozilla Public
+  License, v. 2.0. If a copy of the MPL was not distributed with this
+  file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+  This Source Code Form is "Incompatible With Secondary Licenses", as
+  defined by the Mozilla Public License, v. 2.0.
diff --git a/door-sshproto/examples/kex1.rs b/door-sshproto/examples/kex1.rs
index 3db7c687dba8439ab5e9db9de9623ebcd99d015c..242ddca34c14aa793e730aba0976c7bfc5365781 100644
--- a/door-sshproto/examples/kex1.rs
+++ b/door-sshproto/examples/kex1.rs
@@ -1,15 +1,20 @@
-use anyhow::{Result, Error, Context};
+#![allow(unused_imports)]
 
-use door_sshproto::packets::KexInit;
+use anyhow::{Context, Error, Result};
+use pretty_hex::PrettyHex;
+
+use door_sshproto::packets::*;
+use door_sshproto::wireformat::BinString;
 
 fn main() -> Result<(), Error> {
     let k = KexInit {
-        cookie: &[1,2,3],
+        // cookie: &[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16],
+        cookie: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
         kex: "hello,more".into(),
         hostkey: "hello,more".into(),
-        enc_c2s: "hello,more".into(),
-        enc_s2c: "hello,more".into(),
-        mac_c2s: "hello,more".into(),
+        cipher_c2s: "hello,more".into(),
+        cipher_s2c: "hello,more".into(),
+        mac_c2s: "hi".into(),
         mac_s2c: "hello,more".into(),
         comp_c2s: "hello,more".into(),
         comp_s2c: "hello,more".into(),
@@ -18,15 +23,38 @@ fn main() -> Result<(), Error> {
         first_follows: false,
         reserved: 0,
     };
-    println!("kex1 {k:?}");
-    let t = toml::to_string(&k)?;
-    println!("as toml:\n{}", t);
+    let p = Packet::KexInit(k);
+    println!("p {p:?}");
+
+    let bs = BinString(&[0x11, 0x22, 0x33]);
+    let dhc =
+        Packet::KexDHInit(KexDHInit::Curve25519Init(Curve25519Init { q_c: bs }));
+    println!("dhc1 {dhc:?}");
+
+    // if let SpecificPacket::KexInit(ref k2) = p.p {
+    //     let t = toml::to_string(&k2)?;
+    //     println!("as toml:\n{}", t);
+
+    // }
     // let k2: KexInit = toml::from_str(&t).context("deser")?;
     // println!("kex2 {k:?}");
 
     let mut buf = vec![0; 2000];
-    let written = door_sshproto::wireformat::write_ssh(&mut buf, &k)?;
+    let written = door_sshproto::wireformat::write_ssh(&mut buf, &p)?;
     buf.truncate(written);
-    println!("buf is {buf:#?}");
+    println!("{:?}", buf.hex_dump());
+    let ctx = door_sshproto::packets::ParseContext::new();
+    let x: Packet = door_sshproto::wireformat::packet_from_bytes(&buf, &ctx)?;
+    println!("fetched {x:?}");
+
+    // let mut buf = vec![0; 2000];
+    // let written = door_sshproto::wireformat::write_ssh(&mut buf, &dhc)?;
+    // buf.truncate(written);
+    // println!("wrote {written} {:?}", buf.hex_dump());
+    // let mut ctx = door_sshproto::packets::ParseContext::new();
+    // ctx.kextype = KexType::Curve25519;
+    // let x: Packet = door_sshproto::wireformat::packet_from_bytes(&buf, ctx)?;
+    // println!("fetched {:?}", buf.hex_dump());
+    // println!("fetched {x:?} {:p}", &x);
     Ok(())
 }
diff --git a/door-sshproto/src/conn.rs b/door-sshproto/src/conn.rs
new file mode 100644
index 0000000000000000000000000000000000000000..2ecbef7dbb5771fc44972024f5fa7975a706c1ef
--- /dev/null
+++ b/door-sshproto/src/conn.rs
@@ -0,0 +1,129 @@
+#[allow(unused_imports)]
+use {
+    crate::error::Error,
+    log::{debug, error, info, log, trace, warn},
+};
+
+use ring::digest::Digest;
+
+use crate::*;
+use encrypt::KeyState;
+use packets::Packet;
+use traffic::Traffic;
+
+pub struct Runner<'a> {
+    conn: Conn<'a>,
+    /// Binary packet handling
+    traffic: Traffic<'a>,
+}
+
+impl<'a> Runner<'a> {
+    pub fn new(conn: Conn<'a>, iobuf: &'a mut [u8]) -> Self {
+        Runner {
+            conn,
+            traffic: traffic::Traffic::new(iobuf),
+        }
+    }
+
+    pub fn input(&mut self, buf: &[u8]) -> Result<usize, Error> {
+        let (size, payload) = self.traffic.input(&mut self.conn.keys, &mut self.conn.remote_version, buf)?;
+        if let Some(payload) = payload {
+            if matches!(self.conn.state, ConnState::Ident) {
+                self.conn.state = ConnState::PreKex;
+            }
+            self.conn.handle_payload(payload)?
+        }
+        Ok(size)
+    }
+
+}
+
+/// The core state of a SSH instance.
+pub struct Conn<'a> {
+    state: ConnState,
+
+    /// In-progress kex state
+    kex: Option<kex::Kex>,
+
+    /// Current encryption/integrity keys
+    keys: KeyState,
+
+    /// TODO: Digest is sized to fit 512 bits, we only need 256 for ours currently?
+    sess_id: Option<Digest>,
+
+    pub(crate) is_client: bool,
+
+    pub(crate) algo_conf: kex::AlgoConfig<'a>,
+
+    /// Remote version string. Kept for later kexinit rekeying
+    pub(crate) remote_version: ident::RemoteVersion,
+
+    parse_ctx: packets::ParseContext,
+}
+
+enum ConnState {
+    /// Prior to SSH binary packet protocol, receiving remote version identification
+    Ident,
+    /// Binary packet protocol has started, KexInit not yet received
+    PreKex,
+    /// At any time between receiving KexInit and NewKeys. Can occur multiple times
+    /// at later key exchanges
+    InKex,
+    /// After first NewKeys, prior to auth success
+    PreAuth,
+    /// After auth success
+    Auth,
+    // Cleanup ??
+}
+
+impl<'a> Conn<'a> {
+    /// [`iobuf`] must be sized to fit the largest SSH packet allowed
+    pub fn new() -> Self {
+        let is_client = true; // TODO= true;
+        Conn {
+            kex: None,
+            keys: KeyState::new_cleartext(),
+            sess_id: None,
+            remote_version: ident::RemoteVersion::new(),
+            parse_ctx: packets::ParseContext::new(),
+            state: ConnState::Ident,
+            algo_conf: kex::AlgoConfig::new(is_client),
+            is_client,
+        }
+    }
+
+    /// Consumes an input payload
+    pub(crate) fn handle_payload(&mut self, payload: &[u8]) -> Result<(), Error> {
+        if matches!(self.state, ConnState::Ident) {
+            return Err(Error::Bug);
+        }
+        self.keys.next_seq_decrypt();
+        trace!("bef");
+        let p = wireformat::packet_from_bytes(payload, &self.parse_ctx)?;
+        trace!("handle_payload() got {p:#?}");
+        self.dispatch_packet(&p)?;
+        Ok(())
+    }
+
+    fn dispatch_packet(&mut self, packet: &Packet) -> Result<(), Error> {
+        // TODO: perhaps could consolidate packet allowed checks into a separate function
+        // to run first?
+        match packet {
+            Packet::KexInit(p) => {
+                if matches!(self.state, ConnState::InKex) {
+                    return Err(Error::PacketWrong);
+                }
+                self.kex.get_or_insert_with(kex::Kex::new).handle_kexinit(
+                    self.is_client,
+                    &self.algo_conf,
+                    &self.remote_version,
+                    p,
+                )
+            }
+            p => {
+                warn!("Unhandled packet {p:?}");
+                Err(Error::UnknownPacket)
+            }
+        }
+    }
+}
diff --git a/door-sshproto/src/encrypt.rs b/door-sshproto/src/encrypt.rs
new file mode 100644
index 0000000000000000000000000000000000000000..6a06d2d7f68461af4703dc8bcb7074b930c586d3
--- /dev/null
+++ b/door-sshproto/src/encrypt.rs
@@ -0,0 +1,471 @@
+#[allow(unused_imports)]
+use {
+    crate::error::Error,
+    log::{debug, error, info, log, trace, warn},
+};
+
+use core::num::Wrapping;
+use ring::aead::chacha20_poly1305_openssh as chapoly;
+use aes::cipher::{KeyIvInit, KeySizeUser, BlockSizeUser, StreamCipher};
+use hmac::{Hmac, Mac};
+use sha2::Digest;
+
+use crate::kex;
+use crate::*;
+
+// TODO: check that Ctr32 is sufficient. Should be OK with SSH rekeying.
+type Aes256Ctr32BE = ctr::Ctr32BE<aes::Aes256>;
+type HmacSha256 = hmac::Hmac<sha2::Sha256>;
+
+
+// RFC4253 Section 6. Including length u32 length field, excluding MAC
+const SSH_MIN_PACKET_SIZE: usize = 16;
+const SSH_MIN_PADLEN: usize = 4;
+const SSH_MIN_BLOCK: usize = 8;
+pub const SSH_LENGTH_SIZE: usize = 4;
+
+/// Stateful [`Keys`], stores a sequence number as well
+pub(crate) struct KeyState {
+    keys: Keys,
+    // Packet sequence numbers. These must be transferred to subsequent KeyState
+    // since they don't reset with rekeying.
+    seq_encrypt: Wrapping<u32>,
+    seq_decrypt: Wrapping<u32>,
+}
+
+impl KeyState {
+    /// A brand new `KeyState` with no encryption, zero sequence numbers
+    pub fn new_cleartext() -> Self {
+        KeyState {
+            keys: Keys::new_cleartext(),
+            seq_encrypt: Wrapping(0),
+            seq_decrypt: Wrapping(0),
+        }
+    }
+
+    pub fn next_seq_decrypt(&mut self) {
+        self.seq_decrypt += 1;
+    }
+
+    /// Decrypts the first block in the buffer, returning the length.
+    pub fn decrypt_first_block(&mut self, buf: &mut [u8]) -> Result<u32, Error> {
+        self.keys.decrypt_first_block(buf, self.seq_decrypt.0)
+    }
+
+    /// Decrypt bytes 4 onwards of the buffer and validate AEAD Tag or MAC.
+    /// Ensures that the packet meets minimum length.
+    pub fn decrypt<'b>(&mut self, buf: &'b mut [u8]) -> Result<(), Error> {
+        self.keys.decrypt(buf, self.seq_decrypt.0)
+    }
+
+    /// [`buf`] is the entire output buffer to encrypt in place.
+    /// payload_len is the length of the payload portion
+    /// This is stateful, updating the sequence number.
+    pub fn encrypt<'b>(
+        &mut self, payload_len: usize, buf: &'b mut [u8],
+    ) -> Result<usize, Error> {
+        let e = self.keys.encrypt(payload_len, buf, self.seq_encrypt.0);
+        self.seq_encrypt += 1;
+        e
+    }
+    pub fn size_integ_enc(&self) -> usize {
+        self.keys.integ_enc.size_out()
+    }
+    pub fn size_integ_dec(&self) -> usize {
+        self.keys.integ_dec.size_out()
+    }
+    pub fn size_block_enc(&self) -> usize {
+        self.keys.enc.size_block()
+    }
+    pub fn size_block_dec(&self) -> usize {
+        self.keys.dec.size_block()
+    }
+}
+
+pub(crate) struct Keys {
+    pub(crate) enc: EncKey,
+    pub(crate) dec: DecKey,
+
+    pub(crate) integ_enc: IntegKey,
+    pub(crate) integ_dec: IntegKey,
+}
+
+impl Keys {
+    // pub(crate) fn new(
+    //     enc: EncKey, dec: DecKey, integ_enc: IntegKey, integ_dec: IntegKey,
+    // ) -> Self {
+    //     Keys { enc, dec, integ_enc, integ_dec }
+    // }
+
+    pub fn new_cleartext() -> Self {
+        Keys {
+            enc: EncKey::NoCipher,
+            dec: DecKey::NoCipher,
+            integ_enc: IntegKey::NoInteg,
+            integ_dec: IntegKey::NoInteg,
+        }
+    }
+
+    /// Decrypts the first block in the buffer, returning the length.
+    /// Whether bytes `buf[4..block_size]` are decrypted depends on the cipher, they may be
+    /// handled later by [`decrypt`]. Bytes `buf[0..4]` may not be modified.
+    pub fn decrypt_first_block(
+        &mut self, buf: &mut [u8], seq: u32,
+    ) -> Result<u32, Error> {
+        if buf.len() < self.dec.size_block() {
+            return Err(Error::Bug);
+        }
+        let buf4: [u8; 4] = buf[0..4].try_into().unwrap();
+
+        let d4 = match &mut self.dec {
+            DecKey::ChaPoly(openkey) => {
+                openkey.decrypt_packet_length(seq, buf4);
+                &buf4
+            }
+            DecKey::Aes256Ctr(a) => {
+                a.apply_keystream(&mut buf[..16]);
+                buf[..4].try_into().unwrap()
+            }
+            DecKey::NoCipher => &buf4,
+        };
+        Ok(u32::from_be_bytes(*d4))
+    }
+
+    /// Decrypt bytes 4 onwards of the buffer and validate AEAD Tag or MAC.
+    /// Ensures that the packet meets minimum length.
+    /// The first block_size bytes may have been already decrypted by
+    /// [`decrypt_first_block`]
+    /// depending on the cipher.
+    pub fn decrypt(&mut self, buf: &mut [u8], seq: u32) -> Result<(), Error> {
+        let size_block = self.dec.size_block();
+        let size_integ = self.integ_dec.size_out();
+        if buf.len() < size_block {
+            return Err(Error::BadDecrypt);
+        }
+        if 4 + buf.len() - size_integ < SSH_MIN_PACKET_SIZE {
+            return Err(Error::SSHProtoError);
+        }
+        // "MUST be a multiple of the cipher block size".
+        // encrypted length for aead ciphers doesn't include the length prefix.
+        let len = if self.dec.is_aead() { 0 } else { SSH_LENGTH_SIZE } + buf.len()
+            - size_integ;
+
+        if len % size_block != 0 {
+            return Err(Error::SSHProtoError);
+        }
+
+        let (data, mac) = buf.split_at_mut(buf.len() - size_integ);
+
+        // TODO: ETM modes would check integrity here.
+
+        match &mut self.dec {
+            DecKey::ChaPoly(openkey) => {
+                let mac: &mut [u8; chapoly::TAG_LEN] =
+                    mac.try_into().map_err(|_| Error::Bug)?;
+
+                openkey
+                    .open_in_place(seq, data, mac)
+                    .map_err(|_| Error::BadDecrypt)?;
+            }
+            DecKey::Aes256Ctr(a) => {
+                a.apply_keystream(data);
+            }
+            DecKey::NoCipher => {}
+        }
+
+        match self.integ_dec {
+            IntegKey::ChaPoly => {}
+            IntegKey::NoInteg => {}
+            IntegKey::HmacSha256(k) => {
+                // new_from_slice can't fail.
+                let mut h = HmacSha256::new_from_slice(&k).unwrap();
+                h.update(data);
+                h.verify_slice(mac)
+                .map_err(|_| Error::BadDecrypt)?;
+            }
+        }
+        Ok(())
+    }
+
+    /// Padding is required to meet
+    /// - minimum packet length
+    /// - minimum padding size,
+    /// - encrypted length being a multiple of block length
+    fn get_encrypt_pad(&self, payload_len: usize) -> usize {
+        let size_block = self.enc.size_block();
+        let size_integ = self.integ_enc.size_out();
+        // aead ciphers don't include the initial length field in encrypted blocks
+        let len =
+            1 + payload_len + if self.enc.is_aead() { 0 } else { SSH_LENGTH_SIZE };
+
+        // round padding length upwards so that len is a multiple of block size
+        let mut padlen = self.enc.size_block() - len % self.enc.size_block();
+
+        // need at least 4 bytes padding
+        if padlen < SSH_MIN_PADLEN {
+            padlen += self.enc.size_block()
+        }
+
+        // The minimum size of a packet is 16 (plus mac)
+        // We know we already have at least 8 bytes because of blocksize rounding.
+        if SSH_LENGTH_SIZE + 1 + payload_len + padlen < SSH_MIN_PACKET_SIZE {
+            padlen += self.enc.size_block()
+        }
+        padlen
+    }
+
+    /// Encrypt a buffer in-place, adding packet size, padding, MAC etc.
+    /// Returns the total length.
+    /// Ensures that the packet meets minimum and other length requirements.
+    pub fn encrypt(
+        &mut self, payload_len: usize, buf: &mut [u8], seq: u32,
+    ) -> Result<usize, Error> {
+        let size_block = self.enc.size_block();
+        let size_integ = self.integ_enc.size_out();
+        let padlen = self.get_encrypt_pad(payload_len);
+        // len is everything except the MAC
+        let len = SSH_LENGTH_SIZE + 1 + payload_len + padlen;
+
+        if self.enc.is_aead() {
+            debug_assert_eq!((len - SSH_LENGTH_SIZE) % size_block, 0);
+        } else {
+            debug_assert_eq!(len % size_block, 0);
+        };
+
+        if len + size_integ > buf.len() {
+            error!("Output buffer {} is too small for packet", buf.len());
+            return Err(Error::Bug);
+        }
+
+        // write the length
+        buf[0..SSH_LENGTH_SIZE]
+            .copy_from_slice(&((len - SSH_LENGTH_SIZE) as u32).to_be_bytes());
+        // write random padding
+        let pad_start = SSH_LENGTH_SIZE+1+payload_len;
+        debug_assert_eq!(pad_start+padlen, len);
+        random::fill_random(&mut buf[pad_start..pad_start+padlen]);
+
+        let (enc, rest) = buf.split_at_mut(len);
+        let (mac, _) = rest.split_at_mut(size_integ);
+
+        match self.integ_enc {
+            IntegKey::ChaPoly => {}
+            IntegKey::NoInteg => {}
+            IntegKey::HmacSha256(k) => {
+                // new_from_slice can't fail.
+                let mut h = HmacSha256::new_from_slice(&k).unwrap();
+                h.update(enc);
+                let result = h.finalize();
+                mac.copy_from_slice(&result.into_bytes());
+            }
+        }
+
+        match &mut self.enc {
+            EncKey::ChaPoly(sealkey) => {
+                let mac: &mut [u8; chapoly::TAG_LEN] =
+                    mac.try_into().map_err(|_| Error::Bug)?;
+
+                sealkey.seal_in_place(seq, enc, mac);
+            }
+            EncKey::Aes256Ctr(a) => {
+                a.apply_keystream(enc);
+            }
+            EncKey::NoCipher => {}
+        }
+
+        // ETM modes would go here.
+
+        Ok(len + size_integ)
+    }
+}
+
+/// Placeholder for a cipher type prior to creating a a [`EncKey`] or [`DecKey`],
+/// for use during key setup in [`kex`]
+pub(crate) enum Cipher {
+    ChaPoly,
+    Aes256Ctr,
+    // TODO Aes gcm etc
+}
+
+impl Cipher {
+    /// Creates a cipher key by algorithm name. Must be passed a known name.
+    pub fn from_name(name: &str) -> Result<Self, Error> {
+        use crate::kex::*;
+        match name {
+            SSH_NAME_CHAPOLY => Ok(Cipher::ChaPoly),
+            SSH_NAME_SSH_NAME_AES256_CTR => Ok(Cipher::Aes256Ctr),
+            _ => Err(Error::Bug),
+        }
+    }
+    pub fn key_len(&self) -> usize {
+        match self {
+            Cipher::ChaPoly => chapoly::KEY_LEN,
+            Cipher::Aes256Ctr => aes::Aes256::key_size(),
+        }
+    }
+    pub fn iv_len(&self) -> usize {
+        match self {
+            Cipher::ChaPoly => 0,
+            Cipher::Aes256Ctr => aes::Aes256::block_size(),
+        }
+    }
+    /// Returns the [`Integ`] for this cipher, or None if not aead
+    pub fn integ(&self) -> Option<Integ> {
+        match self {
+            Cipher::ChaPoly => Some(Integ::ChaPoly),
+            Cipher::Aes256Ctr => None,
+        }
+    }
+}
+
+pub(crate) enum EncKey {
+    ChaPoly(chapoly::SealingKey),
+    Aes256Ctr(Aes256Ctr32BE),
+    // AesGcm(Todo?)
+    // AesCtr(Todo?)
+    NoCipher,
+}
+
+impl EncKey {
+    /// Construct a key
+    pub fn from_cipher(cipher: &Cipher, key: &[u8], iv: &[u8]) -> Result<Self, Error> {
+        match cipher {
+            Cipher::ChaPoly => {
+                let key: &[u8; 64] = key.try_into().map_err(|_| Error::Bug)?;
+                Ok(EncKey::ChaPoly(chapoly::SealingKey::new(key)))
+            }
+            Cipher::Aes256Ctr => {
+                let key: &[u8; 32] = key.try_into().map_err(|_| Error::Bug)?;
+                let iv: &[u8; 16] = iv.try_into().map_err(|_| Error::Bug)?;
+                Ok(EncKey::Aes256Ctr(Aes256Ctr32BE::new(key.into(), iv.into())))
+            }
+        }
+    }
+    pub fn is_aead(&self) -> bool {
+        match self {
+            EncKey::ChaPoly(_) => true,
+            EncKey::Aes256Ctr(_a) => false,
+            EncKey::NoCipher => false,
+        }
+    }
+    pub fn size_block(&self) -> usize {
+        match self {
+            EncKey::ChaPoly(_) => SSH_MIN_BLOCK,
+            EncKey::Aes256Ctr(_) => aes::Aes256::block_size(),
+            EncKey::NoCipher => SSH_MIN_BLOCK,
+        }
+    }
+}
+
+pub(crate) enum DecKey {
+    ChaPoly(chapoly::OpeningKey),
+    Aes256Ctr(Aes256Ctr32BE),
+    // AesGcm256
+    // AesCtr256
+    NoCipher,
+}
+
+impl DecKey {
+    /// Construct a key
+    pub fn from_cipher(cipher: &Cipher, key: &[u8], iv: &[u8]) -> Result<Self, Error> {
+        match cipher {
+            Cipher::ChaPoly => {
+                let key: &[u8; 64] = key.try_into().map_err(|_| Error::Bug)?;
+                Ok(DecKey::ChaPoly(chapoly::OpeningKey::new(key)))
+            }
+            Cipher::Aes256Ctr => {
+                let key: &[u8; 32] = key.try_into().map_err(|_| Error::Bug)?;
+                let iv: &[u8; 16] = iv.try_into().map_err(|_| Error::Bug)?;
+                Ok(DecKey::Aes256Ctr(Aes256Ctr32BE::new(key.into(), iv.into())))
+            }
+        }
+    }
+    pub fn is_aead(&self) -> bool {
+        match self {
+            DecKey::ChaPoly(_) => true,
+            DecKey::Aes256Ctr(_a) => false,
+            DecKey::NoCipher => false,
+        }
+    }
+    pub fn size_block(&self) -> usize {
+        match self {
+            DecKey::ChaPoly(_) => SSH_MIN_BLOCK,
+            DecKey::Aes256Ctr(_) => aes::Aes256::block_size(),
+            DecKey::NoCipher => SSH_MIN_BLOCK,
+        }
+    }
+}
+
+pub(crate) enum Integ {
+    ChaPoly,
+    HmacSha256,
+    // aesgcm?
+}
+
+/// Placeholder for a [`IntegKey`] type prior to keying. For use during key setup in [`kex`]
+impl Integ {
+    /// Matches a MAC name. Should not be called for AEAD ciphers, instead use [`EncKey::integ`] etc
+    pub fn from_name(name: &str) -> Result<Self, Error> {
+        // TODO: match standalone HMAC names here.
+        match name {
+            SSH_NAME_HMAC_SHA256 => Ok(Integ::HmacSha256),
+            _ => Err(Error::Bug),
+        }
+    }
+}
+
+pub(crate) enum IntegKey {
+    ChaPoly,
+    HmacSha256([u8; 32]),
+    // aesgcm?
+    // Sha2Hmac ?
+    NoInteg,
+}
+
+impl IntegKey {
+    pub fn from_integ(integ: Integ, key: &[u8]) -> Self {
+        match integ {
+            Integ::ChaPoly => IntegKey::ChaPoly,
+            Integ::HmacSha256 => {
+                // hmac new_from_slice() can't fail.
+                let h = HmacSha256::new_from_slice(key).unwrap();
+                IntegKey::HmacSha256(key.try_into().unwrap())
+            }
+        }
+    }
+    pub fn size_out(&self) -> usize {
+        match self {
+            IntegKey::ChaPoly => chapoly::TAG_LEN,
+            IntegKey::HmacSha256(_) => sha2::Sha256::output_size(),
+            IntegKey::NoInteg => 0,
+        }
+    }
+}
+#[cfg(test)]
+mod tests {
+    use crate::encrypt::{Keys, SSH_LENGTH_SIZE};
+    use crate::error::Error;
+    #[allow(unused_imports)]
+    use log::{debug, error, info, log, trace, warn};
+
+    #[test]
+    fn roundtrip_nocipher() {
+        // check padding works
+        let keys = Keys::new_cleartext();
+        for i in 0usize..40 {
+            let mut v: std::vec::Vec<u8> = (0u8..i as u8 + 30).collect();
+            let orig_payload = v[SSH_LENGTH_SIZE..SSH_LENGTH_SIZE + i].to_vec();
+            let seq = 123u32.rotate_left(i as u32); // something arbitrary
+
+            let written = keys.encrypt(i, v.as_mut_slice(), seq).unwrap();
+
+            v.truncate(written);
+            let l =
+                keys.decrypt_first_block(v.as_mut_slice(), seq).unwrap() as usize;
+            keys.decrypt(v.as_mut_slice(), seq).unwrap();
+            let dec_payload = v[SSH_LENGTH_SIZE..SSH_LENGTH_SIZE + i].to_vec();
+            assert_eq!(written, l + SSH_LENGTH_SIZE + keys.integ_enc.size_out());
+            assert_eq!(orig_payload, dec_payload);
+        }
+    }
+}
diff --git a/door-sshproto/src/error.rs b/door-sshproto/src/error.rs
index 1e30c4ad670726057968dd17a31553e9895241ab..624c9c704e2e7e62a9b807982bf148a96826e4ec 100644
--- a/door-sshproto/src/error.rs
+++ b/door-sshproto/src/error.rs
@@ -1,22 +1,88 @@
-use snafu::{prelude::*};
+use core::str::Utf8Error;
 
-#[derive(Snafu,Debug)]
+use snafu::prelude::*;
+
+// TODO: can we make Snafu not require Debug?
+#[non_exhaustive]
+#[derive(Snafu, Debug)]
 pub enum Error {
-    /// Bad serialize
-    BadSerialize,
-    /// Buffer ran out of room
-    NoSpace,
+    /// Output buffer ran out of room
+    NoRoom,
+
+    /// Input buffer ran out
+    RanOut,
+
     /// Not implemented (unused in SSH protocol)
     NoSerializer,
-    /// Custom error
-    Custom,
+
+    /// Not a UTF8 string
+    BadString,
+
+    /// Decryption failure or integrity mismatch
+    BadDecrypt,
+
+    /// Error in received SSH protocol
+    SSHProtoError,
+
+    /// Unknown packet type
+    UnknownPacket,
+
+    /// Received packet at a disallowed time
+    PacketWrong,
+
+    /// No matching algorithm
+    AlgoNoMatch { algo: &'static str },
+
+    /// Packet size too large (or bad decrypt)
+    BigPacket,
+
+    /// Random number generation failure
+    RngError,
+
+    /// Other custom error
+    Custom { msg: &'static str },
+
+    /// Program bug.
+    /// This state should not be reached, previous logic should have prevented it.
+    Bug,
 }
 
+impl Error {
+    pub fn msg(m: &'static str) -> Error {
+        Error::Custom { msg: m }
+    }
+}
+
+impl From<Utf8Error> for Error {
+    fn from(e: Utf8Error) -> Error {
+        Error::BadString
+    }
+}
+
+impl serde::de::StdError for Error {}
+
+// TODO: need to figure how to return our own Error variants from serde
+// rather than using serde Error::custom().
 impl serde::ser::Error for Error {
     fn custom<T>(msg: T) -> Self
-        where T:std::fmt::Display {
-            // TODO: something noalloc
-            println!("custom error: {}", msg.to_string());
-            Error::Custom
+    where
+        T: core::fmt::Display,
+    {
+        #[cfg(feature = "std")]
+        println!("custom ser error: {}", msg);
+
+        Error::msg("ser error")
+    }
+}
+
+impl serde::de::Error for Error {
+    fn custom<T>(msg: T) -> Self
+    where
+        T: core::fmt::Display,
+    {
+        #[cfg(feature = "std")]
+        println!("custom de error: {}", msg);
+
+        Error::msg("de error")
     }
 }
diff --git a/door-sshproto/src/ident.rs b/door-sshproto/src/ident.rs
new file mode 100644
index 0000000000000000000000000000000000000000..0ccd21a611b7f0c23b8a690c57b1534870223a14
--- /dev/null
+++ b/door-sshproto/src/ident.rs
@@ -0,0 +1,190 @@
+use crate::error::Error;
+
+pub(crate) const OUR_VERSION: &[u8] = "SSH-2.0-door".as_bytes();
+
+const SSH_PREFIX: &[u8] = "SSH-2.0-".as_bytes();
+
+// RFC4253 4.2 says max length 255 incl CR LF.
+// TODO find what's in the wild
+const MAX_REMOTE_VERSION_LEN: usize = 253;
+
+const CR: u8 = 0x0d;
+const LF: u8 = 0x0a;
+
+/// Parses and stores the remove SSH version string
+pub struct RemoteVersion {
+    storage: [u8; MAX_REMOTE_VERSION_LEN],
+    /// Parse state
+    st: VersPars,
+}
+
+/// Version parsing state.
+/// We need to match
+/// `SSH-2.0-softwareversion SP comments CR LF`
+/// at the start of a line. The server may first send other lines
+/// which are discarded.
+// TODO: SSH impls advertising SSH1 compatibility will have "SSH-1.99-" instead.
+// We may need to handle parsing that as well for compatibility. It's possible
+// they aren't common or important these days.
+
+#[derive(Debug)]
+pub(crate) enum VersPars {
+    /// Reading start of a line, before receiving a full SSH-2.0- prefix
+    Start(usize),
+    /// Have a line that didn't start with SSH-2.0-, discarding until LF
+    Discarding,
+    /// Currently reading a SSH-2.0- string, waiting for ending CR
+    FillSSH(usize),
+    /// Have ending CR after a version, Waiting for ending LF
+    HaveCR(usize),
+    /// Completed string.
+    Done(usize),
+}
+
+impl<'a> RemoteVersion {
+    pub fn new() -> Self {
+        RemoteVersion {
+            storage: [0; MAX_REMOTE_VERSION_LEN],
+            st: VersPars::Start(0),
+        }
+    }
+
+    /// Returns the parsed version if stored.
+    pub fn version(&'a self) -> Option<&'a [u8]> {
+        match &self.st {
+            VersPars::Done(len) => {
+                let (s, _) = self.storage.split_at(*len);
+                Some(s)
+            }
+            _ => None,
+        }
+    }
+
+    /// Reads the initial SSH stream to find the version string and returns
+    /// the number of bytes consumed, with bool set if complete.
+    /// Behaviour is undefined if called later after an error.
+    pub fn consume(&mut self, buf: &[u8]) -> Result<(usize, bool), Error> {
+        // consume input byte by byte, feeding through the states
+        let mut taken = 0;
+        for &b in buf {
+            match self.st {
+                VersPars::Done(_) => {}
+                _ => taken += 1,
+            }
+
+            match self.st {
+                VersPars::Start(ref mut pos) => {
+                    let w = self.storage.get_mut(*pos).ok_or(Error::NoRoom)?;
+                    *w = b;
+                    *pos += 1;
+                    // Check if line so far matches SSH-2.0-
+                    let (s, _) = self.storage.split_at(*pos);
+                    if s == SSH_PREFIX {
+                        self.st = VersPars::FillSSH(*pos)
+                    } else if *pos <= SSH_PREFIX.len() {
+                        let (ssh, _) = SSH_PREFIX.split_at(*pos);
+                        if ssh != s {
+                            self.st = VersPars::Discarding
+                        }
+                    } else {
+                        self.st = VersPars::Discarding
+                    }
+                }
+
+                VersPars::Discarding => {
+                    if b == LF {
+                        self.st = VersPars::Start(0);
+                    }
+                }
+
+                VersPars::FillSSH(ref mut pos) => match b {
+                    CR => {
+                        let (s, _) = self.storage.split_at(*pos);
+                        if !s.is_ascii() {
+                            return Err(Error::msg("bad remote version"));
+                        }
+                        self.st = VersPars::HaveCR(*pos);
+                    }
+                    LF => {
+                        return Err(Error::msg("bad remote version"));
+                    }
+                    _ => {
+                        let w = self.storage.get_mut(*pos).ok_or(Error::NoRoom)?;
+                        *w = b;
+                        *pos += 1;
+                    }
+                },
+                VersPars::HaveCR(len) => {
+                    match b {
+                        LF => self.st = VersPars::Done(len),
+                        _ => return Err(Error::msg("bad remote version")),
+                    };
+                }
+
+                VersPars::Done(_) => {
+                    break;
+                }
+            }
+        }
+        // Ran out of input
+        let done = matches!(self.st, VersPars::Done(_));
+        Ok((taken, done))
+    }
+}
+
+#[cfg(test)]
+#[rustfmt::skip]
+mod tests {
+    use crate::ident;
+    use crate::error::Error;
+
+    fn test_version(v: &str, split: usize, expect: &str) -> Result<usize, Error> {
+        let mut r = ident::RemoteVersion::new();
+
+        let split = split.min(v.len());
+        let (a, b) = v.as_bytes().split_at(split);
+
+        let (taken1, done1) = r.consume(a)?;
+        let (taken2, done2) = r.consume(b)?;
+
+        if done1 {
+            assert!(done2);
+            assert!(taken2 == 0);
+        }
+        if taken2 > 0 {
+            assert_eq!(taken1, a.len());
+        }
+
+        let v = core::str::from_utf8(r.version().ok_or(Error::Bug)?)?;
+        assert_eq!(v, expect);
+        Ok(taken1 + taken2)
+    }
+
+    #[test]
+    /// check round trip of packet enums is right
+    fn version() -> Result<(), Error> {
+        let long = core::str::from_utf8(&[60u8; 300]).unwrap();
+        // split input at various positions
+        let splits = [
+            (0..40).collect(),
+            vec![200,252,253,254,255,256],
+        ].concat();
+        for &i in splits.iter() {
+            test_version("SSH-2.0-@\x0d\x0a", i, "SSH-2.0-@").unwrap();
+            test_version("SSH-2.0-good something SSH-2.0-trick\x0d\x0azzz", i, "SSH-2.0-good something SSH-2.0-trick").unwrap();
+            test_version("SSH-2.0-@\x0a\x0d", i, "").unwrap_err();
+            test_version("SSH-2.0-@\x0a\x0d", i, "").unwrap_err();
+            test_version("bleh \x0d\x0aSSH-2.0-@\x0d\x0a", i, "SSH-2.0-@").unwrap();
+            assert_eq!(test_version("SSH-2.0-@\x0d\x0amore", i, "SSH-2.0-@").unwrap(), 11);
+            assert_eq!(test_version("\x0d\x0aSSH-2.0-@\x0d\x0amore", i, "SSH-2.0-@").unwrap(), 13);
+            test_version("\x0d\x0aSSH-2.0bleh \x0d\x0aSSH-2.0-@\x0d\x0a", i, "SSH-2.0-@").unwrap();
+
+            test_version(&long, i, "").unwrap_err();
+            test_version(&format!("{long}\x0d\x0aSSH-2.0-works\x0d\x0a"), i, "SSH-2.0-works").unwrap();
+            test_version(&format!("{long}    \x0aSSH-2.0-works\x0d\x0a"), i, "SSH-2.0-works").unwrap();
+            // a CR by itself is insufficient
+            test_version(&format!("{long}     \x0dSSH-2.0-works\x0d\x0a"), i, "").unwrap_err();
+        }
+        Ok(())
+    }
+}
diff --git a/door-sshproto/src/kex.rs b/door-sshproto/src/kex.rs
new file mode 100644
index 0000000000000000000000000000000000000000..ecf663c3a45ca10dfe57d7c45e9281ae43b6d4a1
--- /dev/null
+++ b/door-sshproto/src/kex.rs
@@ -0,0 +1,306 @@
+use crate::encrypt::{Cipher, Integ};
+use crate::ident::RemoteVersion;
+use crate::namelist::LocalNames;
+use crate::*;
+use ring::digest::{self, Context as DigestCtx, Digest};
+#[allow(unused_imports)]
+use {
+    crate::error::Error,
+    log::{debug, error, info, log, trace, warn},
+};
+
+// RFC8731
+pub const SSH_NAME_CURVE25519: &str = "curve25519-sha256";
+// An older alias prior to standardisation. Eventually could be removed
+pub const SSH_NAME_CURVE25519_LIBSSH: &str = "curve25519-sha256@libssh.org";
+// RFC8308 Extension Negotiation
+pub const SSH_NAME_EXT_INFO_S: &str = "ext-info-s";
+pub const SSH_NAME_EXT_INFO_C: &str = "ext-info-c";
+
+// RFC8709
+pub const SSH_NAME_ED25519: &str = "curve25519-sha256";
+// RFC8332
+pub const SSH_NAME_RSA_SHA256: &str = "rsa-sha2-256";
+// RFC4253
+pub const SSH_NAME_RSA_SHA1: &str = "ssh-rsa";
+
+// RFC4344
+pub const SSH_NAME_AES256_CTR: &str = "aes256-ctr";
+// OpenSSH PROTOCOL.chacha20poly1305.txt
+pub const SSH_NAME_CHAPOLY: &str = "chacha20-poly1305@openssh.com";
+// OpenSSH PROTOCOL.
+pub const SSH_NAME_AES256_GCM: &str = "aes256-gcm@openssh.com";
+// (No-one uses aes-gcm RFC5647 from the NSA, it fails to define mac negotiation
+// sensibly and has horrible naming style)
+
+// RFC6668
+pub const SSH_NAME_HMAC_SHA256: &str = "hmac-sha2-256";
+
+// RFC4253
+pub const SSH_NAME_NONE: &str = "none";
+
+const empty_localnames: LocalNames = LocalNames(&[]);
+
+// TODO this will be configurable.
+const fixed_options_kex: LocalNames =
+    LocalNames(&[SSH_NAME_CURVE25519, SSH_NAME_CURVE25519_LIBSSH]);
+const fixed_options_hostkey: LocalNames =
+    LocalNames(&[SSH_NAME_ED25519, SSH_NAME_RSA_SHA256, SSH_NAME_RSA_SHA1]);
+
+const fixed_options_cipher: LocalNames = LocalNames(&[SSH_NAME_CHAPOLY, SSH_NAME_AES256_CTR]);
+const fixed_options_mac: LocalNames = LocalNames(&[SSH_NAME_HMAC_SHA256]);
+const fixed_options_comp: LocalNames = LocalNames(&[SSH_NAME_NONE]);
+
+pub(crate) struct AlgoConfig<'a> {
+    kexs: LocalNames<'a>,
+    hostkeys: LocalNames<'a>,
+    ciphers: LocalNames<'a>,
+    macs: LocalNames<'a>,
+    comps: LocalNames<'a>,
+}
+
+impl<'a> AlgoConfig<'a> {
+    /// Creates the standard algorithm configuration
+    /// TODO: ext-info-s and ext-info-c
+    pub fn new(is_client: bool) -> Self {
+        AlgoConfig {
+            kexs: fixed_options_kex,
+            hostkeys: fixed_options_hostkey,
+            ciphers: fixed_options_cipher,
+            macs: fixed_options_mac,
+            comps: fixed_options_comp,
+        }
+    }
+}
+
+#[allow(non_snake_case)]
+pub(crate) struct Kex {
+    // TODO: we could be tricky here and have an enum that saves memory
+    // by only keeping currently required fields. to be done once the structure
+    // stabilises
+
+    // Cookie sent in our KexInit packet. Kept so that we can reproduce the
+    // KexInit packet when calculating the exchange hash.
+    our_cookie: [u8; 16],
+
+    // populated once we have sent and received KexInit
+    algos: Option<Algos>,
+    // kexhash state. progessively include version idents, kexinit payloads, hostkey, e/f, secret
+    hash_ctx: Option<DigestCtx>,
+
+    // populated after kex reply, from hash_ctx
+    H: Option<Digest>,
+}
+
+enum KexState {
+    New,
+    SentKexInit,
+    RecvKexInit,
+    //... todo
+}
+
+/// Records the chosen algorithms while key exchange proceeds
+struct Algos {
+    kex: SharedSecret,
+    // hostkey: HostKey,
+    cipher_enc: Cipher,
+    cipher_dec: Cipher,
+    integ_enc: Integ,
+    integ_dec: Integ,
+}
+
+impl Kex {
+    pub fn new() -> Self {
+        let mut our_cookie = [0u8; 16];
+        random::fill_random(our_cookie.as_mut_slice());
+        Kex { our_cookie, algos: None, hash_ctx: None, H: None }
+    }
+    pub fn handle_kexinit(
+        &mut self, is_client: bool, algo_conf: &AlgoConfig,
+        remote_version: &RemoteVersion, remote_kexinit: &packets::KexInit,
+    ) -> Result<(), Error> {
+        let algos = Self::algo_negotiation(is_client, remote_kexinit, algo_conf)?;
+        self.hash_ctx = Some(self.start_kexhash(
+            &algos,
+            is_client,
+            algo_conf,
+            remote_version,
+            remote_kexinit,
+        )?);
+        self.algos = Some(algos);
+        Ok(())
+    }
+
+    pub fn make_kexinit<'a>(&self, conf: &'a AlgoConfig) -> packets::KexInit<'a> {
+        packets::KexInit {
+            cookie: self.our_cookie,
+            kex: (&conf.kexs).into(),
+            hostkey: (&conf.hostkeys).into(),
+            cipher_c2s: (&conf.ciphers).into(),
+            cipher_s2c: (&conf.ciphers).into(),
+            mac_c2s: (&conf.macs).into(),
+            mac_s2c: (&conf.macs).into(),
+            comp_c2s: (&conf.comps).into(),
+            comp_s2c: (&conf.comps).into(),
+            lang_c2s: (&empty_localnames).into(),
+            lang_s2c: (&empty_localnames).into(),
+            first_follows: false,
+            reserved: 0,
+        }
+    }
+
+    fn start_kexhash(
+        &mut self, algos: &Algos, is_client: bool, algo_conf: &AlgoConfig, remote_version: &RemoteVersion,
+        remote_kexinit: &packets::KexInit,
+    ) -> Result<DigestCtx, Error> {
+        // RFC4253 section 8:
+        // The hash H is computed as the HASH hash of the concatenation of the
+        // following:
+        //    string    V_C, the client's identification string (CR and LF
+        //              excluded)
+        //    string    V_S, the server's identification string (CR and LF
+        //              excluded)
+        //    string    I_C, the payload of the client's SSH_MSG_KEXINIT
+        //    string    I_S, the payload of the server's SSH_MSG_KEXINIT
+        //    string    K_S, the host key
+        //    mpint     e, exchange value sent by the client
+        //    mpint     f, exchange value sent by the server
+        //    mpint     K, the shared secret
+
+        let mut hash_ctx = DigestCtx::new(algos.kex.get_hash());
+        let remote_version = remote_version.version().ok_or(Error::Bug)?;
+        // Recreate our own kexinit packet to hash
+        let own_kexinit = self.make_kexinit(algo_conf);
+        if is_client {
+            hash_ctx.update(ident::OUR_VERSION);
+            hash_ctx.update(remote_version);
+            wireformat::hash_ssh(&mut hash_ctx, &own_kexinit)?;
+            wireformat::hash_ssh(&mut hash_ctx, remote_kexinit)?;
+        } else {
+            hash_ctx.update(remote_version);
+            hash_ctx.update(ident::OUR_VERSION);
+            wireformat::hash_ssh(&mut hash_ctx, remote_kexinit)?;
+            wireformat::hash_ssh(&mut hash_ctx, &own_kexinit)?
+        }
+        // The remainder of hash_ctx is updated after kexdhreply
+
+        Ok(hash_ctx)
+    }
+
+    /// Perform SSH algorithm negotiation
+    fn algo_negotiation(
+        is_client: bool, p: &packets::KexInit, conf: &AlgoConfig,
+    ) -> Result<Algos, Error> {
+        // For each algorithm we select the first name in the client's
+        // list that is also present in the server's list.
+        let kex_method = p
+            .kex
+            .first_protocol_match(is_client, &conf.kexs)?
+            .ok_or(Error::AlgoNoMatch { algo: "kex" })?;
+        let kex = SharedSecret::from_name(kex_method)?;
+        let hostkey_method = p
+            .hostkey
+            .first_protocol_match(is_client, &conf.hostkeys)?
+            .ok_or(Error::AlgoNoMatch { algo: "hostkey" })?;
+
+        // Switch between client/server tx/rx
+        let c2s = (&p.cipher_c2s, &p.mac_c2s, &p.comp_c2s);
+        let s2c = (&p.cipher_s2c, &p.mac_s2c, &p.comp_s2c);
+        let ((cipher_tx, mac_tx, comp_tx), (cipher_rx, mac_rx, comp_rx)) =
+            if is_client { (c2s, s2c) } else { (s2c, c2s) };
+
+        let n = cipher_tx
+            .first_protocol_match(is_client, &conf.ciphers)?
+            .ok_or(Error::AlgoNoMatch { algo: "encryption" })?;
+        let cipher_enc = Cipher::from_name(n)?;
+        let n = cipher_rx
+            .first_protocol_match(is_client, &conf.ciphers)?
+            .ok_or(Error::AlgoNoMatch { algo: "encryption" })?;
+        let cipher_dec = Cipher::from_name(n)?;
+
+        // We ignore mac algorithms for AEAD ciphers
+        let integ_enc = if let Some(integ) = cipher_enc.integ() {
+            integ
+        } else {
+            let n = mac_tx
+                .first_protocol_match(is_client, &conf.macs)?
+                .ok_or(Error::AlgoNoMatch { algo: "mac" })?;
+            Integ::from_name(n)?
+        };
+        let integ_dec = if let Some(integ) = cipher_dec.integ() {
+            integ
+        } else {
+            let n = mac_rx
+                .first_protocol_match(is_client, &conf.macs)?
+                .ok_or(Error::AlgoNoMatch { algo: "mac" })?;
+            Integ::from_name(n)?
+        };
+
+        // Compression only matches "none", we don't need further handling
+        // at the moment.
+        comp_tx
+            .first_protocol_match(is_client, &conf.comps)?
+            .ok_or(Error::AlgoNoMatch { algo: "compression" })?;
+        comp_rx
+            .first_protocol_match(is_client, &conf.comps)?
+            .ok_or(Error::AlgoNoMatch { algo: "compression" })?;
+
+        // Ignore language fields at present. unsure which implementations
+        // use it, possibly SunSSH
+
+        Ok(Algos { kex, cipher_enc, cipher_dec, integ_enc, integ_dec })
+    }
+}
+
+enum SharedSecret {
+    KexCurve25519(KexCurve25519),
+    // ECDH?
+}
+
+impl SharedSecret {
+    pub fn from_name(name: &str) -> Result<Self, Error> {
+        match name {
+            SSH_NAME_CURVE25519 | SSH_NAME_CURVE25519_LIBSSH => {
+                Ok(SharedSecret::KexCurve25519(KexCurve25519::new()))
+            }
+            _ => Err(Error::Bug),
+        }
+    }
+    pub fn get_hash(&self) -> &'static digest::Algorithm {
+        match self {
+            SharedSecret::KexCurve25519(_) => &digest::SHA256,
+        }
+    }
+}
+
+struct KexCurve25519 {}
+
+impl KexCurve25519 {
+    fn new() -> Self {
+        KexCurve25519 {}
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::encrypt;
+    use crate::error::Error;
+    use crate::kex;
+
+    #[test]
+    fn test_name_match() {
+        // check that the from_name() functions are complete
+        for k in kex::fixed_options_kex.0.iter() {
+            println!("{k}");
+            kex::SharedSecret::from_name(k).unwrap();
+        }
+        for k in kex::fixed_options_cipher.0.iter() {
+            println!("{k}");
+            encrypt::Cipher::from_name(k).unwrap();
+        }
+        for k in kex::fixed_options_mac.0.iter() {
+            println!("{k}");
+            encrypt::Integ::from_name(k).unwrap();
+        }
+    }
+}
diff --git a/door-sshproto/src/lib.rs b/door-sshproto/src/lib.rs
index 056bb2ef0bb78c08c37f827e2c1ad2847ba216b4..797cc458133d901bf5060e8f3080d29dc4a8f5a4 100644
--- a/door-sshproto/src/lib.rs
+++ b/door-sshproto/src/lib.rs
@@ -1,15 +1,21 @@
+// #![no_std]
 #![forbid(unsafe_code)]
+// XXX unused_imports only during dev churn
+#![allow(unused_imports)]
+
+#[cfg(test)]
+#[macro_use]
+extern crate std;
 
 pub mod packets;
-// XXX public?
-pub mod wireformat;
+// XXX decide what is public
+pub mod conn;
+pub mod encrypt;
 pub mod error;
-
-#[cfg(test)]
-mod tests {
-    #[test]
-    fn it_works() {
-        let result = 2 + 2;
-        assert_eq!(result, 4);
-    }
-}
+pub mod ident;
+pub mod kex;
+pub mod test;
+pub mod traffic;
+pub mod wireformat;
+pub mod namelist;
+pub mod random;
diff --git a/door-sshproto/src/namelist.rs b/door-sshproto/src/namelist.rs
new file mode 100644
index 0000000000000000000000000000000000000000..0f19c6c04e1e61a26040706b1b3eec913ab2c083
--- /dev/null
+++ b/door-sshproto/src/namelist.rs
@@ -0,0 +1,171 @@
+//! SSH comma separated algorithm lists.
+#[allow(unused_imports)]
+use {
+    crate::error::Error,
+    log::{debug, error, info, log, trace, warn},
+};
+
+use serde::de;
+use serde::de::{DeserializeSeed, SeqAccess, Visitor};
+use serde::ser::{SerializeSeq, SerializeTuple, Serializer};
+use serde::Deserializer;
+
+use serde::{Deserialize, Serialize};
+
+
+/// A comma separated string, can be deserialized or serialized.
+/// Used for remote name lists.
+#[derive(Serialize, Deserialize, Debug)]
+pub struct StringNames<'a>(pub &'a str);
+
+/// A list of names, can only be serialized. Used for local name lists, comes
+/// from local fixed lists
+/// Deliberately 'static since it should only come from hardcoded local strings
+/// SSH_NAME_* in [`kex`]. We don't validate string contents.
+#[derive(Debug)]
+pub struct LocalNames<'a>(pub &'a[&'static str]);
+
+/// The general form that can store either representation
+#[derive(Serialize, Debug)]
+pub enum NameList<'a> {
+    String(StringNames<'a>),
+    Local(LocalNames<'a>),
+}
+
+impl<'de: 'a, 'a> Deserialize<'de> for NameList<'a> {
+    fn deserialize<D>(deserializer: D) -> Result<NameList<'a>, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        let s = StringNames::deserialize(deserializer)?;
+        if s.0.is_ascii() {
+            Ok(NameList::String(s))
+        } else {
+            Err(de::Error::custom("algorithm isn't ascii"))
+        }
+    }
+}
+
+/// Serialize the list of names with comma separators
+impl<'a> Serialize for LocalNames<'a> {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        let mut seq = serializer.serialize_seq(None)?;
+        let names = &self.0;
+        for i in 0..names.len() {
+            seq.serialize_element(names[i].as_bytes());
+            if i < names.len()-1 {
+                seq.serialize_element(&(',' as u8));
+            }
+        }
+        seq.end()
+    }
+}
+
+impl<'a> From<&'a str> for StringNames<'a> {
+    fn from(s: &'a str) -> Self {
+        Self(s)
+    }
+}
+impl<'a> From<&'a [&'static str]> for LocalNames<'a> {
+    fn from(s: &'a [&'static str]) -> Self {
+        Self(s)
+    }
+}
+impl<'a> From<&'a str> for NameList<'a> {
+    fn from(s: &'a str) -> Self {
+        NameList::String(s.into())
+    }
+}
+impl<'a> Into<NameList<'a>> for &LocalNames<'a> {
+    fn into(self) -> NameList<'a> {
+        NameList::Local(LocalNames(self.0))
+    }
+
+}
+
+impl<'a> NameList<'a> {
+    /// Returns the first name in this namelist that matches, based on SSH priority.
+    /// The SSH client's list (which could be either remote or ours) is used
+    /// to determine priority.
+    /// `self` is a remote list, `our_options` are our own allowed options in preference
+    /// order.
+    /// Must only be called on [`StringNames`], will fail if called with self as [`LocalNames`].
+    pub fn first_protocol_match(
+        &self, is_client: bool, our_options: &LocalNames,
+    ) -> Result<Option<&str>, Error> {
+        match self {
+            NameList::String(s) => {
+                Ok(if is_client {
+                    s.first_match(our_options)
+                } else {
+                    s.first_options_match(our_options)
+                })
+            },
+            NameList::Local(_) => Err(Error::Bug)
+        }
+    }
+}
+
+impl<'a> StringNames<'a> {
+    /// Returns the first name in this namelist that matches one of the provided options
+    fn first_match(&self, options: &LocalNames) -> Option<&str> {
+        trace!("match {:?} options {:?}", self, options);
+        for n in self.0.split(',') {
+            for o in options.0.iter() {
+                trace!("match {} options {} {}", n, *o, (*n == **o));
+                if n == *o {
+                    return Some(n);
+                }
+            }
+        }
+        trace!("None");
+        None
+    }
+
+    /// Returns the first of "options" that is in this namelist
+    fn first_options_match(&self, options: &LocalNames) -> Option<&str> {
+        trace!("firstopmatch {:?} options {:?}", self, options);
+        for o in options.0.iter() {
+            for n in self.0.split(',') {
+                if n == *o {
+                    return Some(n);
+                }
+            }
+        }
+        None
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::{wireformat};
+    use crate::namelist::*;
+    use pretty_hex::PrettyHex;
+
+
+    #[test]
+    fn test_localnames_serialize() {
+        let tests: Vec<&[&str]> = vec![
+            &["foo", "quux", "boo"],
+            &[],
+            &["one"],
+            &["one", "2"],
+            &["", "2"],
+            &["3", ""],
+            &["", ""],
+            &[",", ","], // not really valid
+        ];
+        for t in tests.iter() {
+            let n = NameList::Local(LocalNames(t));
+            let mut buf = vec![99; 30];
+            let l = wireformat::write_ssh(&mut buf, &n).unwrap();
+            buf.truncate(l);
+            let out1 = core::str::from_utf8(&buf).unwrap();
+            // check that a join with std gives the same result.
+            assert_eq!(out1, t.join(","));
+        }
+    }
+}
diff --git a/door-sshproto/src/packets.rs b/door-sshproto/src/packets.rs
index 21674843bdf3a131e789e61f3b83271cfb89aa71..437e2b1093c265a4b04d84c634dfb0ee860c32a6 100644
--- a/door-sshproto/src/packets.rs
+++ b/door-sshproto/src/packets.rs
@@ -1,38 +1,186 @@
-use serde::{Serialize, Deserialize};
+//! SSH protocol packets. A [`Packet`] can be serialized/deserialized to the
+//! SSH Binary Packet Protocol using [`serde`] with [`crate::wireformat`].
+//!
+//! These are mostly container formats though there is some logic to determine
+//! which enum variant needs deserializing for certain packet types.
+//!
+//! Some packet formats are self describing, eg [`UserauthRequest`] has a `method`
+//! string that switches between [`MethodPubkey`] and [`MethodPassword`]. Other packets
+//! such as [`KexDHReply`] don't have that structure, instead they depend on previous
+//! state of the SSH session. That state is passed with [`ParseContext`].
+use core::borrow::BorrowMut;
+use core::cell::Cell;
+use core::fmt;
+use core::marker::PhantomData;
 
-#[derive(Serialize, Deserialize)]
-pub struct Packet<'a> {
-    pub ty: u8,
-    #[serde(borrow)]
-    pub p: SpecificPacket<'a>,
+use serde::de;
+use serde::de::{DeserializeSeed, SeqAccess, Visitor};
+use serde::ser::{SerializeSeq, SerializeTuple, Serializer};
+use serde::Deserializer;
+
+use serde::{Deserialize, Serialize};
+
+use crate::error::Error;
+use crate::namelist::NameList;
+use crate::wireformat::BinString;
+
+/// Negotiated Key Exchange (KEX) type, used to parse kexinit/kexreply packets.
+#[derive(Debug)]
+pub enum KexType {
+    Unset,
+    Curve25519,
+    DiffieHellman,
 }
 
-#[derive(Serialize, Deserialize)]
-pub enum SpecificPacket<'a> {
-    #[serde(borrow)]
-    KexInit(KexInit<'a>),
-    KexDHInit(KexDHInit),
-    KexDHReply(KexDHReply),
+/// State to be passed to deserialisation. Use this so the parser can select the correct
+/// enum variant to deserialize.
+pub struct ParseContext {
+    pub kextype: KexType,
 }
 
-// TODO: impl matching
- // XXX - how does a str reference work? prob needs to be [u8]?
-#[derive(Serialize, Deserialize, Debug)]
-pub struct NameList<'a>(&'a str);
+impl ParseContext {
+    pub fn new() -> Self {
+        ParseContext { kextype: KexType::Unset }
+    }
+}
+
+/// State passed as the Deserializer seed.
+pub(crate) struct PacketState<'a> {
+    pub ctx: &'a ParseContext,
+    // Private fields that keep state during parsing.
+    // TODO Perhaps not actually necessary, could be removed and just pass ParseContext?
+    // pub(crate) ty: Cell<Option<MessageNumber>>,
+}
+
+#[derive(Debug)]
+#[repr(u8)]
+#[allow(non_camel_case_types)]
+pub enum MessageNumber {
+    SSH_MSG_KEXINIT = 20,
+    SSH_MSG_KEXDH_INIT = 30,
+    SSH_MSG_KEXDH_REPLY = 31,
+    SSH_MSG_USERAUTH_REQUEST = 50,
+}
+
+impl TryFrom<u8> for MessageNumber {
+    type Error = Error;
+    fn try_from(v: u8) -> Result<Self, Error> {
+        match v {
+            20 => Ok(MessageNumber::SSH_MSG_KEXINIT),
+            30 => Ok(MessageNumber::SSH_MSG_KEXDH_INIT),
+            31 => Ok(MessageNumber::SSH_MSG_KEXDH_REPLY),
+            50 => Ok(MessageNumber::SSH_MSG_USERAUTH_REQUEST),
+            _ => Err(Error::UnknownPacket),
+        }
+    }
+}
 
-impl<'a> From<&'a str> for NameList<'a> {
-    fn from(s: &'a str) -> Self {
-        Self(s)
+/// Some packets require context to parse, so we pass PacketState
+pub(crate) struct DeserPacket<'a>(pub(crate) &'a PacketState<'a>);
+
+impl<'de: 'a, 'a> DeserializeSeed<'de> for DeserPacket<'a> {
+    type Value = Packet<'de>;
+    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        struct PacketVisitor<'b> {
+            seed: &'b PacketState<'b>,
+        }
+
+        impl<'de: 'b, 'b> Visitor<'de> for PacketVisitor<'b> {
+            type Value = Packet<'de>;
+
+            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+                formatter.write_str("struct Packet")
+            }
+            fn visit_seq<V>(self, mut seq: V) -> Result<Packet<'de>, V::Error>
+            where
+                V: SeqAccess<'de>,
+            {
+                // First byte is always message number
+                let msg_num: u8 = seq
+                    .next_element()?
+                    .ok_or_else(|| de::Error::invalid_length(0, &self))?;
+                let ty: MessageNumber = msg_num
+                    .try_into()
+                    .map_err(|_| de::Error::custom("Unknown packet type"))?;
+
+                // Decode based on the message number
+                let p = match ty {
+                    MessageNumber::SSH_MSG_KEXINIT => Packet::KexInit(
+                        seq.next_element()?
+                            .ok_or_else(|| de::Error::invalid_length(1, &self))?,
+                    ),
+                    MessageNumber::SSH_MSG_KEXDH_INIT => Packet::KexDHInit(
+                        seq.next_element_seed(DeserKexDHInit(self.seed))?
+                            .ok_or_else(|| de::Error::invalid_length(1, &self))?,
+                    ),
+                    MessageNumber::SSH_MSG_KEXDH_REPLY => Packet::KexDHReply(
+                        seq.next_element_seed(DeserKexDHReply(self.seed))?
+                            .ok_or_else(|| de::Error::invalid_length(1, &self))?,
+                    ),
+                    MessageNumber::SSH_MSG_USERAUTH_REQUEST => todo!("userauth"),
+                };
+
+                Ok(p)
+            }
+        }
+        deserializer.deserialize_seq(PacketVisitor { seed: self.0 })
+    }
+}
+
+impl<'a> Serialize for Packet<'a> {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        let mut seq = serializer.serialize_seq(None)?;
+
+        match self {
+            Packet::KexInit(p) => {
+                let t = MessageNumber::SSH_MSG_KEXINIT as u8;
+                seq.serialize_element(&t)?;
+                seq.serialize_element(p)?;
+            }
+            Packet::KexDHInit(p) => {
+                let t = MessageNumber::SSH_MSG_KEXDH_INIT as u8;
+                seq.serialize_element(&t)?;
+                seq.serialize_element(p)?;
+            }
+            Packet::KexDHReply(p) => {
+                let t = MessageNumber::SSH_MSG_KEXDH_REPLY as u8;
+                seq.serialize_element(&t)?;
+                seq.serialize_element(p)?;
+            }
+            Packet::UserauthRequest(p) => {
+                let t = MessageNumber::SSH_MSG_USERAUTH_REQUEST as u8;
+                seq.serialize_element(&t)?;
+                seq.serialize_element(p)?;
+            }
+        };
+
+        seq.end()
     }
 }
 
+/// Top level SSH packet enum
+#[derive(Debug)]
+pub enum Packet<'a> {
+    KexInit(KexInit<'a>),
+    KexDHInit(KexDHInit<'a>),
+    KexDHReply(KexDHReply<'a>),
+    UserauthRequest(UserauthRequest<'a>),
+}
+
 #[derive(Serialize, Deserialize, Debug)]
 pub struct KexInit<'a> {
-    pub cookie: &'a [u8], // 16 bytes
+    pub cookie: [u8; 16],
+    #[serde(borrow)]
     pub kex: NameList<'a>,
     pub hostkey: NameList<'a>,
-    pub enc_c2s: NameList<'a>,
-    pub enc_s2c: NameList<'a>,
+    pub cipher_c2s: NameList<'a>,
+    pub cipher_s2c: NameList<'a>,
     pub mac_c2s: NameList<'a>,
     pub mac_s2c: NameList<'a>,
     pub comp_c2s: NameList<'a>,
@@ -40,24 +188,135 @@ pub struct KexInit<'a> {
     pub lang_c2s: NameList<'a>,
     pub lang_s2c: NameList<'a>,
     pub first_follows: bool,
-    pub reserved: u32
+    pub reserved: u32,
+}
+
+#[derive(Serialize, Debug)]
+pub enum KexDHInit<'a> {
+    Curve25519Init(Curve25519Init<'a>),
+    DiffieHellmanInit(DiffieHellmanInit),
 }
 
-#[derive(Serialize, Deserialize)]
-pub enum KexDHInit {
-    Curve25519Init(Curve25519Init),
+/// Deserialize implementation  for KexDHInit
+struct DeserKexDHInit<'a>(&'a PacketState<'a>);
+
+impl<'de: 'a, 'a> DeserializeSeed<'de> for DeserKexDHInit<'a> {
+    type Value = KexDHInit<'de>;
+    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        // Use the algo variant that was negotiated in KEX
+        match self.0.ctx.kextype {
+            KexType::Curve25519 => Ok(KexDHInit::Curve25519Init(
+                Curve25519Init::deserialize(deserializer)?,
+            )),
+            KexType::DiffieHellman => Ok(KexDHInit::DiffieHellmanInit(
+                DiffieHellmanInit::deserialize(deserializer)?,
+            )),
+            KexType::Unset => Err(de::Error::custom("kextype not set")),
+        }
+    }
+}
+
+#[derive(Serialize, Debug)]
+pub enum KexDHReply<'a> {
+    Curve25519Reply(Curve25519Reply<'a>),
+    DiffieHellmanReply(DiffieHellmanReply<'a>),
+}
+
+/// Deserialize implementation  for KexDHReply
+struct DeserKexDHReply<'a>(&'a PacketState<'a>);
+
+impl<'de: 'a, 'a> DeserializeSeed<'de> for DeserKexDHReply<'a> {
+    type Value = KexDHReply<'de>;
+    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        // Use the algo variant that was negotiated in KEX
+        match self.0.ctx.kextype {
+            KexType::Curve25519 => Ok(KexDHReply::Curve25519Reply(
+                Curve25519Reply::deserialize(deserializer)?,
+            )),
+            KexType::DiffieHellman => Ok(KexDHReply::DiffieHellmanReply(
+                DiffieHellmanReply::deserialize(deserializer)?,
+            )),
+            KexType::Unset => Err(de::Error::custom("kextype not set")),
+        }
+    }
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+pub struct Curve25519Init<'a> {
+    #[serde(borrow)]
+    pub q_c: BinString<'a>,
+}
+#[derive(Serialize, Deserialize, Debug)]
+pub struct Curve25519Reply<'a> {
+    #[serde(borrow)]
+    pub k_s: BinString<'a>,
+    pub q_s: BinString<'a>,
+    pub sig: BinString<'a>,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+pub struct DiffieHellmanInit {
+    pub e: u32,
+}
+#[derive(Serialize, Deserialize, Debug)]
+pub struct DiffieHellmanReply<'a> {
+    #[serde(borrow)]
+    pub k_s: BinString<'a>,
+    pub f: BinString<'a>, // mpint
+    pub sig: BinString<'a>,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+pub struct UserauthRequest<'a> {
+    pub username: &'a str,
+    pub service: &'a str,
+    pub method: &'a str,
+    // TODO: need to deserialize AuthMethod enum
+    pub a: AuthMethod<'a>,
 }
 
-#[derive(Serialize, Deserialize)]
-pub enum KexDHReply {
-    Curve25519Reply(Curve25519Reply),
+/// The method-specific part of a [`UserauthRequest`].
+#[derive(Serialize, Deserialize, Debug)]
+pub enum AuthMethod<'a> {
+    #[serde(borrow)]
+    Password(MethodPassword<'a>),
+    Pubkey(MethodPubkey<'a>),
 }
 
-#[derive(Serialize, Deserialize)]
-pub struct Curve25519Init {
+#[derive(Serialize, Deserialize, Debug)]
+pub struct MethodPassword<'a> {
+    pub change: bool,
+    pub password: &'a str,
+}
 
+#[derive(Serialize, Deserialize, Debug)]
+pub struct MethodPubkey<'a> {
+    pub trial: bool,
+    pub algo: &'a str,
+    pub pubkey: &'a [u8],
+    // TODO: need to deserialize sig as an Option
+    pub sig: Option<&'a [u8]>,
 }
-#[derive(Serialize, Deserialize)]
-pub struct Curve25519Reply {
+
+#[cfg(test)]
+mod tests {
+    use crate::{packets,wireformat};
+
+    #[test]
+    /// check round trip of packet enums is right
+    fn packet_type() {
+        for i in 0..=255 {
+            let ty = packets::MessageNumber::try_from(i);
+            if let Ok(ty) = ty {
+                assert_eq!(i, ty as u8);
+            }
+        }
+    }
 
 }
diff --git a/door-sshproto/src/random.rs b/door-sshproto/src/random.rs
new file mode 100644
index 0000000000000000000000000000000000000000..202581a3c7a2584da10eaa490a023ea9c2335670
--- /dev/null
+++ b/door-sshproto/src/random.rs
@@ -0,0 +1,8 @@
+use rand::RngCore;
+use crate::error::Error;
+
+pub fn fill_random(buf: &mut [u8]) -> Result<(), Error> {
+    // TODO: can this return an error?
+    rand::rngs::OsRng.fill_bytes(buf);
+    Ok(())
+}
diff --git a/door-sshproto/src/test.rs b/door-sshproto/src/test.rs
new file mode 100644
index 0000000000000000000000000000000000000000..b0b77cad19a437ef1b454732f7f4b6210b0e4884
--- /dev/null
+++ b/door-sshproto/src/test.rs
@@ -0,0 +1,67 @@
+#[cfg(test)]
+mod tests {
+    use crate::error::Error;
+    use crate::packets::*;
+    use crate::wireformat::BinString;
+    use crate::packets::{KexType, Packet};
+    use crate::{packets, wireformat};
+    use pretty_hex::PrettyHex;
+    use serde::de::Unexpected;
+    use serde::{Deserialize, Serialize};
+
+    fn test_roundtrip_packet(
+        p: &Packet, kextype: packets::KexType,
+    ) -> Result<(), Error> {
+        let mut buf1 = vec![99; 500];
+        let _w1 = wireformat::write_ssh(&mut buf1, &p)?;
+
+        let mut ctx = packets::ParseContext::new();
+        ctx.kextype = kextype;
+
+        let p2 = wireformat::packet_from_bytes(&buf1, &ctx)?;
+
+        let mut buf2 = vec![99; 500];
+        let _w2 = wireformat::write_ssh(&mut buf2, &p2)?;
+        // println!("{p:?}");
+        // println!("{p2:?}");
+        // println!("{:?}", buf1.hex_dump());
+        // println!("{:?}", buf2.hex_dump());
+
+        assert_eq!(buf1, buf2);
+        Ok(())
+    }
+
+    #[test]
+    fn roundtrip_kexinit() {
+        let k = KexInit {
+            cookie: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
+            kex: "kex".into(),
+            hostkey: "hostkey,another".into(),
+            cipher_c2s: "chacha20-poly1305@openssh.com,aes128-ctr".into(),
+            cipher_s2c: "blowfish".into(),
+            mac_c2s: "hmac-sha1".into(),
+            mac_s2c: "hmac-md5".into(),
+            comp_c2s: "none".into(),
+            comp_s2c: "".into(),
+            lang_c2s: "".into(),
+            lang_s2c: "".into(),
+            first_follows: true,
+            reserved: 0x6148291e,
+        };
+        let p = Packet::KexInit(k);
+        test_roundtrip_packet(&p, KexType::Unset).unwrap();
+    }
+
+    #[test]
+    fn roundtrip_packet_kexdh() {
+        // XXX: this should break later if the q_c length is
+        let bs = BinString(&[0x11, 0x22, 0x33]);
+        let p =
+            Packet::KexDHInit(KexDHInit::Curve25519Init(Curve25519Init { q_c: bs }));
+
+        test_roundtrip_packet(&p, KexType::Curve25519).unwrap();
+        // packet format needs to be consistent
+        // test_roundtrip_packet(&p, KexType::DiffieHellman).unwrap_err();
+        // test_roundtrip_packet(&p, KexType::Unset).unwrap_err();
+    }
+}
diff --git a/door-sshproto/src/traffic.rs b/door-sshproto/src/traffic.rs
new file mode 100644
index 0000000000000000000000000000000000000000..32c4cd5b454a56b69d9e482a82146e9b6cb1118f
--- /dev/null
+++ b/door-sshproto/src/traffic.rs
@@ -0,0 +1,200 @@
+#[allow(unused_imports)]
+use {
+    crate::error::Error,
+    log::{debug, error, info, log, trace, warn},
+};
+
+use crate::encrypt::SSH_LENGTH_SIZE;
+use crate::encrypt::KeyState;
+use crate::ident::{RemoteVersion};
+use crate::*;
+
+pub(crate) struct Traffic<'a> {
+    // TODO: if smoltcp exposed both ends of a CircularBuffer to recv()
+    // we could perhaps just work directly in smoltcp's provided buffer?
+    // Would need changes to ring chapoly_openssh and block ciphers.
+
+    // TODO: decompression will need another buffer
+
+    /// Accumulated input or output buffer.
+    /// Should be sized to fit the largest packet allowed.
+    /// Contains ciphertext or cleartext, encrypted/decrypted in-place.
+    buf: &'a mut [u8],
+    state: TrafState,
+}
+
+/// State machine for reads/writes sharing [`Traffic::buf`]
+#[derive(Debug)]
+enum TrafState {
+    /// buf is unused
+    Idle,
+
+    /// Reading remote version, not SSH packet format
+    Version,
+    /// Reading initial block for packet length. idx > 0.
+    ReadInitial {
+        idx: usize,
+    },
+    /// Reading remainder of encrypted packet
+    Read {
+        idx: usize,
+        expect: usize,
+    },
+    /// Whole encryped packet has been read
+    ReadComplete {
+        len: usize,
+    },
+    /// Decrypted complete input payload
+    InPayload {
+        len: usize,
+    },
+
+    OutPayload {
+        len: usize,
+    },
+    /// Encrypted, writing to the socket
+    Write {
+        idx: usize,
+        len: usize,
+    },
+}
+
+impl<'a> Traffic<'a> {
+    pub fn new(buf: &'a mut [u8]) -> Self {
+        Traffic {
+            buf,
+            state: TrafState::Version,
+        }
+    }
+
+    /// Returns the number of bytes consumed, and optionally
+    /// a complete packet payload.
+    pub fn input(&mut self, keys: &mut KeyState, remote_version: &mut RemoteVersion,
+        buf: &[u8]) -> Result<(usize, Option<&[u8]>), Error> {
+        let mut inlen = 0;
+        trace!("input() state: {:?}", self.state);
+        if let TrafState::Version = self.state {
+            // handle initial version string
+            let (l, done) = remote_version.consume(buf)?;
+            trace!("input() l: {l} {done}");
+            if done {
+                self.state = TrafState::Idle
+            }
+            inlen += l;
+        }
+        let buf = &buf[inlen..];
+
+        inlen += self.fill_input(keys, buf)?;
+
+        trace!("input() state2: {:?}", self.state);
+        let payload = if let TrafState::InPayload { len } = self.state {
+        use pretty_hex::PrettyHex;
+            let payload = &self.buf[SSH_LENGTH_SIZE + 1..SSH_LENGTH_SIZE + 1 + len];
+            self.state = TrafState::Idle;
+        trace!("pyload {:?}", &payload.hex_dump());
+            Some(payload)
+        } else {
+            None
+        };
+
+
+        trace!("input() state2: {:?}", self.state);
+
+        Ok((inlen, payload))
+    }
+
+    /// Write any pending output, returning the size written
+    pub fn output(&mut self, keys: &mut KeyState, buf: &mut [u8]) -> Result<usize, Error> {
+        if let TrafState::OutPayload { len } = self.state {
+            // Payload ready, encrypt it
+            let len = keys.encrypt(len, self.buf)?;
+            self.state = TrafState::Write { idx: 0, len };
+        }
+
+        match self.state {
+            TrafState::Write { ref mut idx, len } => {
+                let wlen = (len - *idx).min(buf.len());
+                buf.copy_from_slice(&self.buf[*idx..*idx + wlen]);
+                *idx += wlen;
+
+                if *idx == len {
+                    // all done, read the next packet
+                    self.state = TrafState::Idle
+                }
+                Ok(wlen)
+            }
+            _ => Ok(0),
+        }
+    }
+
+    fn fill_input(&mut self, keys: &mut KeyState, buf: &[u8]) -> Result<usize, Error> {
+        let size_block = keys.size_block_dec();
+        let size_integ = keys.size_integ_dec();
+        // 'r' is the remaining input, a slice that moves along.
+        // Used to calculate the size to return
+        let mut r = buf;
+
+        // Either Idle with input, or filling the initial block
+        if let Some(idx) = match self.state {
+            TrafState::Idle if r.len() > 0 => Some(0),
+            TrafState::ReadInitial { idx } => Some(idx),
+            _ => None,
+        } {
+            let need = (size_block - idx).clamp(0, r.len());
+            let x;
+            (x, r) = r.split_at(need);
+            let w = &mut self.buf[idx..idx + need];
+            w.copy_from_slice(x);
+            self.state = TrafState::ReadInitial { idx: idx + need }
+        }
+
+        // Have enough input now to decrypt the packet length
+        if let TrafState::ReadInitial { idx } = self.state {
+            if idx >= size_block {
+                let w = &mut self.buf[..size_block];
+                let total_len =
+                    keys
+                        .decrypt_first_block(w)?
+                        .checked_add((SSH_LENGTH_SIZE + size_integ) as u32)
+                        .ok_or(Error::BadDecrypt)? as usize;
+
+                if total_len > self.buf.len() {
+                    // TODO: Or just BadDecrypt could make more sense if
+                    // it were packet corruption/decryption failure
+                    warn!("total_len {total_len:08x}");
+                    return Err(Error::BigPacket);
+                }
+                self.state = TrafState::Read { idx, expect: total_len }
+            }
+        }
+
+        // Know expected length, read until the end of the packet.
+        // We have already validated that expect_len <= buf_size
+        if let TrafState::Read { ref mut idx, expect } = self.state {
+            let need = (expect - *idx).min(r.len());
+            let x;
+            (x, r) = r.split_at(need);
+            let w = &mut self.buf[*idx..*idx + need];
+            w.copy_from_slice(x);
+            *idx += need;
+            if *idx == expect {
+                self.state = TrafState::ReadComplete { len: expect }
+            }
+        }
+
+        if let TrafState::ReadComplete { len } = self.state {
+            let w = &mut self.buf[SSH_LENGTH_SIZE..len];
+            keys.decrypt(w)?;
+            let padlen = w[0] as usize;
+            trace!("len {len} sub {} padlen {padlen}",
+                SSH_LENGTH_SIZE + 1 + size_integ + padlen);
+            let payload_len = len
+                .checked_sub(SSH_LENGTH_SIZE + 1 + size_integ + padlen)
+                .ok_or(Error::SSHProtoError)?;
+
+            self.state = TrafState::InPayload { len: payload_len }
+        }
+
+        Ok(buf.len() - r.len())
+    }
+}
diff --git a/door-sshproto/src/wireformat.rs b/door-sshproto/src/wireformat.rs
index c302d460f2a048b59578b4cd52e1b2d0357d012e..3a632317ffcc64729bea7fee18a17734b7f5ea39 100644
--- a/door-sshproto/src/wireformat.rs
+++ b/door-sshproto/src/wireformat.rs
@@ -1,56 +1,138 @@
-use serde::{ser, de, Serializer, Serialize, Deserializer, Deserialize};
+//! SSH protocol serialization.
+//! Implements enough of serde to handle the formats defined in [`crate::packets`]
+
+//! See [RFC4251](https://datatracker.ietf.org/doc/html/rfc4251) for encodings,
+//! [RFC4253](https://datatracker.ietf.org/doc/html/rfc4253) and others for packet structure
+use serde::{
+    de, ser,
+    de::{DeserializeSeed, SeqAccess, Visitor},
+    ser::SerializeSeq,
+    Deserialize, Deserializer, Serialize, Serializer,
+};
+
 use crate::error::Error;
-use core::result::{Result};
+use crate::packets::{DeserPacket, Packet, PacketState, ParseContext};
+use core::cell::Cell;
+use core::result::Result;
 use core::slice;
 
-/// See rfc4251 for encodings
+/// Parses a [`Packet`] from a borrowed `&[u8]` byte buffer.
+/// Requires [`ParseContext`] to indicate which Kex variant is being use.
+pub fn packet_from_bytes<'a>(
+    b: &'a [u8], ctx: &ParseContext,
+) -> Result<Packet<'a>, Error> {
+    let seed = PacketState {
+        ctx,
+        // ty: Cell::new(None),
+    };
 
-pub struct SeSSH<'a> {
-    target: &'a mut[u8],
-    pos: usize,
+    let mut ds = DeSSHBytes::from_bytes(b);
+    let t = DeserPacket(&seed).deserialize(&mut ds)?;
+    Ok(t)
+    // TODO check for trailing bytes, pos != b.len()
 }
 
-type Res = Result<(), Error>;
 
-/// Returns the length written.
-// TODO: is there a nicer way? IterMut?
+/// Writes a SSH packet to a buffer. Returns the length written.
 pub fn write_ssh<T>(target: &mut [u8], value: &T) -> Result<usize, Error>
-        where T: Serialize {
-    let mut serializer = SeSSH {
-        target,
-        pos: 0,
-    };
+where
+    T: Serialize,
+{
+    let mut serializer = SeSSHBytes::WriteBytes { target, pos: 0 };
     value.serialize(&mut serializer)?;
-    Ok(serializer.pos)
+    Ok(match serializer {
+        SeSSHBytes::WriteBytes { target: _, pos } => pos,
+        _ => 0, // TODO is there a better syntax here? we know it's always WriteBytes
+    })
+}
+
+/// Hashes the contents of a SSH packet, updating the provided context.
+pub fn hash_ssh<T>(hash_ctx: &mut ring::digest::Context, value: &T) -> Result<(), Error>
+where
+    T: Serialize,
+{
+    let mut serializer = SeSSHBytes::WriteHash { hash_ctx };
+    value.serialize(&mut serializer)?;
+    Ok(())
+}
+
+type Res = Result<(), Error>;
+
+#[derive(Deserialize, Debug)]
+/// A SSH style binary string. 32 bit length followed by bytes.
+pub struct BinString<'a>(pub &'a [u8]);
+
+
+// Helper to pass a serde_state DeserializeState impl where a DeserializeSeed is needed,
+// for next_element_seed() etc.
+// struct SeedForState<'a, T, S> {
+//     seed: &'a mut S,
+//     value: PhantomData<T>,
+// }
+
+// impl<'a, T, S> SeedForState<'a, T, S> {
+//     pub fn new(seed: &'a mut S) -> Self {
+//         Self {
+//             seed,
+//             value: PhantomData,
+//         }
+//     }
+// }
+
+impl<'a> Serialize for BinString<'a> {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        let mut seq = serializer.serialize_seq(None)?;
+        let l = self.0.len() as u32;
+        seq.serialize_element(&l)?;
+        seq.serialize_element(self.0)?;
+        seq.end()
+    }
 }
 
-impl SeSSH<'_> {
 
+/// Serializer for the SSH wire protocol. Writes into a borrowed `&mut [u8]` buffer.
+/// Optionally compute the hash of the packet rather than serializing.
+enum SeSSHBytes<'a> {
+    WriteBytes { target: &'a mut [u8], pos: usize },
+
+    WriteHash { hash_ctx: &'a mut ring::digest::Context }
+}
+
+impl SeSSHBytes<'_> {
     fn push(&mut self, v: &[u8]) -> Res {
-        // TODO: can IterMut be used somehow?
-        if self.pos + v.len() > self.target.len() {
-            return Err(Error::NoSpace);
+        match self {
+            SeSSHBytes::WriteBytes { target, ref mut pos } => {
+                if *pos + v.len() > target.len() {
+                    return Err(Error::NoRoom);
+                }
+                target[*pos..*pos + v.len()].copy_from_slice(v);
+                *pos += v.len();
+            }
+            SeSSHBytes::WriteHash { hash_ctx } => {
+                hash_ctx.update(v);
+            }
         }
-        self.target[self.pos..self.pos + v.len()].copy_from_slice(v);
-        self.pos += v.len();
         Ok(())
     }
 }
 
-impl Serializer for &mut SeSSH<'_> {
+impl Serializer for &mut SeSSHBytes<'_> {
     type Ok = ();
     type Error = crate::error::Error;
 
     type SerializeSeq = Self;
     type SerializeStruct = Self;
-    type SerializeStructVariant = Self;
-    type SerializeTuple = ser::Impossible<(), Error>;
+    type SerializeTuple = Self;
+    type SerializeStructVariant = ser::Impossible<(), Error>;
     type SerializeTupleStruct = ser::Impossible<(), Error>;
     type SerializeTupleVariant = ser::Impossible<(), Error>;
     type SerializeMap = ser::Impossible<(), Error>;
 
     fn serialize_bool(self, v: bool) -> Res {
-        self.serialize_u32(v as u32)
+        self.serialize_u8(v as u8)
     }
     fn serialize_u8(self, v: u8) -> Res {
         self.push(&[v])
@@ -58,47 +140,65 @@ impl Serializer for &mut SeSSH<'_> {
     fn serialize_u32(self, v: u32) -> Res {
         self.push(&v.to_be_bytes())
     }
-    // Not actually used in any SSH packets, mentioned in the arch doc
+    /// Not actually used in any SSH packets, mentioned in the arch doc
     fn serialize_u64(self, v: u64) -> Res {
         self.push(&v.to_be_bytes())
     }
-    fn serialize_str(self, v: &str) -> Res {
-        self.serialize_u32(v.as_bytes().len() as u32)?;
-        self.push(v.as_bytes())
-    }
+    /// Serialize raw bytes with no prefix
     fn serialize_bytes(self, v: &[u8]) -> Res {
-        self.push(v)
+        self.push(v)?;
+        todo!(
+            "This is asymmetric with deserialize_bytes, but isn't currently being used."
+        )
+    }
+    fn serialize_str(self, v: &str) -> Res {
+        let b = v.as_bytes();
+        self.serialize_u32(b.len() as u32)?;
+        self.push(b)
     }
-    // XXX Unsure if we're using Option
     fn serialize_none(self) -> Res {
         Ok(())
     }
     fn serialize_some<T>(self, v: &T) -> Res
-            where T: ?Sized + Serialize {
+    where
+        T: ?Sized + Serialize,
+    {
         v.serialize(self)
     }
     fn serialize_newtype_struct<T>(self, _name: &'static str, v: &T) -> Res
-            where T: ?Sized + Serialize {
+    where
+        T: ?Sized + Serialize,
+    {
         v.serialize(self)
     }
-    fn serialize_newtype_variant<T>(self,
-            _name: &'static str, _variant_index: u32, _variant: &'static str,
-            v: &T) -> Res
-            where T: ?Sized + Serialize {
+    fn serialize_newtype_variant<T>(
+        self, _name: &'static str, _variant_index: u32, _variant: &'static str,
+        v: &T,
+    ) -> Res
+    where
+        T: ?Sized + Serialize,
+    {
         v.serialize(self)
     }
-    fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq, Error> {
+    fn serialize_seq(
+        self, _len: Option<usize>,
+    ) -> Result<Self::SerializeSeq, Error> {
         Ok(self)
     }
-    fn serialize_struct(self, _name: &'static str, _len: usize) -> Result<Self::SerializeSeq, Error> {
+    fn serialize_struct(
+        self, _name: &'static str, _len: usize,
+    ) -> Result<Self::SerializeSeq, Error> {
         Ok(self)
     }
-    fn serialize_struct_variant(self,
-            _name: &'static str, _variant_index: u32, _variant: &'static str,
-            _len: usize) -> Result<Self::SerializeSeq, Error> {
+    fn serialize_tuple(self, _len: usize) -> Result<Self::SerializeTuple, Error> {
         Ok(self)
     }
 
+    // Required for no_std
+    fn collect_str<T: ?Sized>(self, _: &T) -> Res {
+        Err(Error::NoSerializer)
+    }
+
     // Not in the SSH protocol
     fn serialize_i8(self, _: i8) -> Res {
         Err(Error::NoSerializer)
@@ -131,33 +231,42 @@ impl Serializer for &mut SeSSH<'_> {
     fn serialize_unit_struct(self, _name: &'static str) -> Res {
         Err(Error::NoSerializer)
     }
-    fn serialize_unit_variant(self,
-            _name: &'static str, _variant_index: u32, _variant: &'static str) -> Res {
+    fn serialize_unit_variant(
+        self, _name: &'static str, _variant_index: u32, _variant: &'static str,
+    ) -> Res {
         Err(Error::NoSerializer)
     }
-    fn serialize_tuple(self, _len: usize) -> Result<Self::SerializeTuple, Error> {
+    fn serialize_tuple_struct(
+        self, _name: &'static str, _len: usize,
+    ) -> Result<Self::SerializeTupleStruct, Error> {
         Err(Error::NoSerializer)
     }
-    fn serialize_tuple_struct(self, _name: &'static str, _len: usize)
-            -> Result<Self::SerializeTupleStruct, Error> {
+    fn serialize_tuple_variant(
+        self, _name: &'static str, _variant_index: u32, _variant: &'static str,
+        _len: usize,
+    ) -> Result<Self::SerializeTupleVariant, Error> {
         Err(Error::NoSerializer)
     }
-    fn serialize_tuple_variant(self,
-            _name: &'static str, _variant_index: u32, _variant: &'static str,
-            _len: usize) -> Result<Self::SerializeTupleVariant, Error> {
+    fn serialize_map(
+        self, _len: Option<usize>,
+    ) -> Result<Self::SerializeMap, Error> {
         Err(Error::NoSerializer)
     }
-    fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap, Error> {
+    fn serialize_struct_variant(
+        self, _name: &'static str, _variant_index: u32, _variant: &'static str,
+        _len: usize,
+    ) -> Result<Self::SerializeStructVariant, Error> {
         Err(Error::NoSerializer)
     }
 }
 
-impl ser::SerializeSeq for &mut SeSSH<'_> {
+impl ser::SerializeSeq for &mut SeSSHBytes<'_> {
     type Ok = ();
     type Error = crate::error::Error;
 
     fn serialize_element<T>(&mut self, value: &T) -> Result<(), Self::Error>
-            where T: ?Sized + Serialize,
+    where
+        T: ?Sized + Serialize,
     {
         value.serialize(&mut **self)
     }
@@ -167,12 +276,15 @@ impl ser::SerializeSeq for &mut SeSSH<'_> {
     }
 }
 
-impl ser::SerializeStruct for &mut SeSSH<'_> {
+impl ser::SerializeStruct for &mut SeSSHBytes<'_> {
     type Ok = ();
     type Error = crate::error::Error;
 
-    fn serialize_field<T>(&mut self, _key: &'static str, value: &T) -> Result<(), Self::Error>
-            where T: ?Sized + Serialize,
+    fn serialize_field<T>(
+        &mut self, _key: &'static str, value: &T,
+    ) -> Result<(), Self::Error>
+    where
+        T: ?Sized + Serialize,
     {
         value.serialize(&mut **self)
     }
@@ -182,12 +294,13 @@ impl ser::SerializeStruct for &mut SeSSH<'_> {
     }
 }
 
-impl ser::SerializeStructVariant for &mut SeSSH<'_> {
+impl ser::SerializeTuple for &mut SeSSHBytes<'_> {
     type Ok = ();
     type Error = crate::error::Error;
 
-    fn serialize_field<T>(&mut self, _key: &'static str, value: &T) -> Result<(), Self::Error>
-            where T: ?Sized + Serialize,
+    fn serialize_element<T: ?Sized>(&mut self, value: &T) -> Result<(), Self::Error>
+    where
+        T: Serialize,
     {
         value.serialize(&mut **self)
     }
@@ -197,6 +310,228 @@ impl ser::SerializeStructVariant for &mut SeSSH<'_> {
     }
 }
 
-pub struct DeSSH {
+/// Deserializer for the SSH wire protocol, from borrowed `&[u8]`
+/// Implements enough of serde to handle the formats defined in [`crate::packets`]
+struct DeSSHBytes<'a> {
+    input: &'a [u8],
+    pos: usize,
+}
+
+impl<'de> DeSSHBytes<'de> {
+    // XXX: rename to new() ?
+    pub fn from_bytes(input: &'de [u8]) -> Self {
+        DeSSHBytes { input, pos: 0 }
+    }
+    // #[inline]
+    fn take(&mut self, len: usize) -> Result<&'de [u8], Error> {
+        if len > self.input.len() {
+            return Err(Error::RanOut);
+        }
+        let (t, rest) = self.input.split_at(len);
+        self.input = rest;
+        self.pos += len;
+        Ok(t)
+    }
+
+    #[inline]
+    fn parse_u8(&mut self) -> Result<u8, Error> {
+        let t = self.take(core::mem::size_of::<u8>())?;
+        let u = u8::from_be_bytes(t.try_into().unwrap());
+        // println!("deser u8 {u}");
+        Ok(u)
+    }
+
+    #[inline]
+    fn parse_u32(&mut self) -> Result<u32, Error> {
+        let t = self.take(core::mem::size_of::<u32>())?;
+        let u = u32::from_be_bytes(t.try_into().unwrap());
+        // println!("deser u32 {u}");
+        Ok(u)
+    }
+
+    fn parse_u64(&mut self) -> Result<u64, Error> {
+        let t = self.take(core::mem::size_of::<u64>())?;
+        Ok(u64::from_be_bytes(t.try_into().unwrap()))
+    }
+
+    #[inline]
+    fn parse_str(&mut self) -> Result<&'de str, Error> {
+        let len = self.parse_u32()?;
+        let t = self.take(len as usize)?;
+        let s = core::str::from_utf8(t).map_err(|_| Error::BadString)?;
+        Ok(s)
+    }
+}
+
+struct SeqAccessDeSSH<'a, 'b: 'a> {
+    ds: &'a mut DeSSHBytes<'b>,
+    len: Option<usize>,
+}
+
+impl<'a, 'b: 'a> SeqAccess<'b> for SeqAccessDeSSH<'a, 'b> {
+    type Error = Error;
+    #[inline]
+    fn next_element_seed<V: DeserializeSeed<'b>>(
+        &mut self, seed: V,
+    ) -> Result<Option<V::Value>, Error> {
+        if let Some(ref mut len) = self.len {
+            if *len > 0 {
+                *len -= 1;
+                Ok(Some(DeserializeSeed::deserialize(seed, &mut *self.ds)?))
+            } else {
+                Ok(None)
+            }
+        } else {
+            Ok(Some(DeserializeSeed::deserialize(seed, &mut *self.ds)?))
+        }
+    }
+
+    fn size_hint(&self) -> Option<usize> {
+        self.len
+    }
+}
+
+impl<'de, 'a> Deserializer<'de> for &'a mut DeSSHBytes<'de> {
+    type Error = Error;
+
+    fn deserialize_bool<V>(self, visitor: V) -> Result<V::Value, Error>
+    where
+        V: Visitor<'de>,
+    {
+        visitor.visit_bool(self.parse_u8()? != 0)
+    }
+
+    fn deserialize_u8<V>(self, visitor: V) -> Result<V::Value, Error>
+    where
+        V: Visitor<'de>,
+    {
+        visitor.visit_u8(self.parse_u8()?)
+    }
+
+    fn deserialize_u32<V>(self, visitor: V) -> Result<V::Value, Error>
+    where
+        V: Visitor<'de>,
+    {
+        visitor.visit_u32(self.parse_u32()?)
+    }
+
+    fn deserialize_u64<V>(self, visitor: V) -> Result<V::Value, Error>
+    where
+        V: Visitor<'de>,
+    {
+        visitor.visit_u64(self.parse_u64()?)
+    }
+
+    fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Error>
+    where
+        V: Visitor<'de>,
+    {
+        visitor.visit_borrowed_str(self.parse_str()?)
+    }
+
+    /* deserialize_bytes() is like a string but with binary data. it has
+    a u32 prefix of the length. Fixed length byte arrays use _tuple() */
+    fn deserialize_bytes<V>(self, visitor: V) -> Result<V::Value, Error>
+    where
+        V: Visitor<'de>,
+    {
+        let len = self.parse_u32()?;
+        let t = self.take(len as usize)?;
+        visitor.visit_borrowed_bytes(t)
+    }
+
+    fn deserialize_tuple<V>(self, len: usize, visitor: V) -> Result<V::Value, Error>
+    where
+        V: Visitor<'de>,
+    {
+        visitor.visit_seq(SeqAccessDeSSH { ds: self, len: Some(len) })
+    }
+
+    fn deserialize_seq<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: Visitor<'de>,
+    {
+        visitor.visit_seq(SeqAccessDeSSH { ds: self, len: None })
+    }
+
+    fn deserialize_struct<V>(
+        self, _name: &'static str, fields: &'static [&'static str], visitor: V,
+    ) -> Result<V::Value, Error>
+    where
+        V: Visitor<'de>,
+    {
+        self.deserialize_tuple(fields.len(), visitor)
+    }
+
+    fn deserialize_enum<V>(
+        self, _name: &'static str, _variants: &'static [&'static str], _visitor: V,
+    ) -> Result<V::Value, Self::Error>
+    where
+        V: Visitor<'de>,
+    {
+        // visitor.visit_enum(self);
+        panic!("enum")
+    }
+
+    fn deserialize_newtype_struct<V>(
+        self, _name: &'static str, visitor: V,
+    ) -> Result<V::Value, Self::Error>
+    where
+        V: Visitor<'de>,
+    {
+        visitor.visit_newtype_struct(self)
+    }
+
+    fn deserialize_tuple_struct<V>(
+        self, _name: &'static str, _len: usize, _visitor: V,
+    ) -> Result<V::Value, Self::Error>
+    where
+        V: Visitor<'de>,
+    {
+        Err(Error::NoSerializer)
+    }
+
+    fn deserialize_unit<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: Visitor<'de>,
+    {
+        todo!("unit")
+    }
+    // The remainder will fail.
+    serde::forward_to_deserialize_any! {
+        i8 i16 i32 i64 i128 u16 u128 f32 f64 char string
+        byte_buf unit_struct
+        map identifier ignored_any
+        option
+    }
+    fn deserialize_any<V>(self, _visitor: V) -> Result<V::Value, Error>
+    where
+        V: Visitor<'de>,
+    {
+        Err(Error::NoSerializer)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::error::Error;
+    use crate::*;
+    // use pretty_hex::PrettyHex;
+
+    #[test]
+    /// check that hash_ssh() matches hashing a serialized message
+    fn test_hash_ssh() {
+        use ring::digest;
+        let input = "hello";
+        let mut     buf = vec![99; 20];
+        let w1 = wireformat::write_ssh(&mut buf, &input).unwrap();
+        buf.truncate(w1);
+
+        let mut hash_ctx = digest::Context::new(&digest::SHA256);
+        wireformat::hash_ssh(&mut hash_ctx, &input).unwrap();
+        let digest1 = hash_ctx.finish();
+        let digest2 = digest::digest(&digest::SHA256, &buf);
+        assert_eq!(digest1.as_ref(), digest2.as_ref());
+    }
 
 }
diff --git a/door-tokio/Cargo.toml b/door-tokio/Cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..f6497792d4d67261be2c0a74cd44a16c169a7558
--- /dev/null
+++ b/door-tokio/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "door-tokio"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+tokio = { version = "1.17", features = ["full"] }
+door-sshproto = { path = "../door-sshproto", features = ["std"] }
+log = { version = "0.4" }
+
+[dev-dependencies]
+snafu = { version = "0.7", default-features = true }
+anyhow = { version = "1.0" }
+pretty-hex = "0.2"
+simplelog = "0.11"
diff --git a/door-tokio/examples/con1.rs b/door-tokio/examples/con1.rs
new file mode 100644
index 0000000000000000000000000000000000000000..ebdd0f47a999e8fcdb28a6e7ae55de99fd3c456c
--- /dev/null
+++ b/door-tokio/examples/con1.rs
@@ -0,0 +1,64 @@
+#[allow(unused_imports)]
+use {
+    // crate::error::Error,
+    log::{debug, error, info, log, trace, warn},
+};
+use std::error::Error;
+use pretty_hex::PrettyHex;
+use tokio::io::AsyncWriteExt;
+use tokio::net::TcpStream;
+
+use door_sshproto::*;
+
+use simplelog::*;
+
+#[tokio::main]
+async fn main() -> Result<(), Box<dyn Error>> {
+
+    CombinedLogger::init(
+    vec![
+        TermLogger::new(LevelFilter::Trace, Config::default(), TerminalMode::Mixed, ColorChoice::Auto),
+    ]
+    ).unwrap();
+
+    info!("running main");
+    trace!("tracing main");
+
+    // Connect to a peer
+    let mut stream = TcpStream::connect("dropbear.nl:22").await?;
+
+    let mut work = vec![0; 1000];
+    let c = conn::Conn::new();
+    let mut r = conn::Runner::new(c, work.as_mut_slice());
+
+    let mut buf = vec![0; 100];
+    loop {
+        stream.readable().await?;
+        let n = stream.try_read(&mut buf)?;
+        let s = &buf.as_slice()[..n];
+        let l = r.input(s)?;
+        println!("read {l}");
+    }
+
+
+    // let mut d = ident::RemoteVersion::new();
+    // let (taken, done) = d.consume(&buf)?;
+    // println!("taken {taken} done {done}");
+    // let v = d.version();
+    // match v {
+    //     Some(x) => {
+    //         println!("v {:?}", x.hex_dump());
+    //     }
+    //     None => {
+    //         println!("None");
+    //     }
+    // }
+    // let (_, rest) = buf.split_at(taken + 5);
+    // println!("reset {:?}", rest.hex_dump());
+
+    // let ctx = packets::ParseContext::new();
+    // let p = wireformat::packet_from_bytes(rest, &ctx)?;
+    // println!("{p:#?}");
+
+    Ok(())
+}
diff --git a/door-tokio/src/lib.rs b/door-tokio/src/lib.rs
new file mode 100644
index 0000000000000000000000000000000000000000..1b4a90c938387450422982a89aed075aa1e8c010
--- /dev/null
+++ b/door-tokio/src/lib.rs
@@ -0,0 +1,8 @@
+#[cfg(test)]
+mod tests {
+    #[test]
+    fn it_works() {
+        let result = 2 + 2;
+        assert_eq!(result, 4);
+    }
+}