[ssh] Simple SSH and SFTP support

This can be used to connect to Fuchsia devices that support SSH.

Change-Id: I0fadcbaf0670f1f23f332f07becdfb64e56a3b9a
diff --git a/go.mod b/go.mod
index 0d3c6ee..13dfa3a 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,8 @@
 require (
 	github.com/google/subcommands v0.0.0-20181012225330-46f0354f6315
 	github.com/google/uuid v1.1.0
-	go.chromium.org/luci v0.0.0-20181205024016-0c89bd1bcf4f
+	go.chromium.org/luci v0.0.0-20181218015242-20acb618582d
+	golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9
 	golang.org/x/net v0.0.0-20181029044818-c44066c5c816
 	golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35
 )
diff --git a/go.sum b/go.sum
index 664dcc7..3f5cd1e 100644
--- a/go.sum
+++ b/go.sum
@@ -2,8 +2,10 @@
 github.com/google/subcommands v0.0.0-20181012225330-46f0354f6315/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
 github.com/google/uuid v1.1.0 h1:Jf4mxPC/ziBnoPIdpQdPJ9OeiomAUHLvxmPRSPH9m4s=
 github.com/google/uuid v1.1.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-go.chromium.org/luci v0.0.0-20181205024016-0c89bd1bcf4f h1:YHpVpP44+EX/FdtkLuityBjE7JHl6Yhm9SVVToihk+s=
-go.chromium.org/luci v0.0.0-20181205024016-0c89bd1bcf4f/go.mod h1:MIQewVTLvOvc0UioV0JNqTNO/RspKFS0XEeoKrOxsdM=
+go.chromium.org/luci v0.0.0-20181218015242-20acb618582d h1:WWlp6PQtC8FyaxytRO5UBYFBDcPOYy6+o7JmcvgLMuU=
+go.chromium.org/luci v0.0.0-20181218015242-20acb618582d/go.mod h1:MIQewVTLvOvc0UioV0JNqTNO/RspKFS0XEeoKrOxsdM=
+golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0=
+golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/net v0.0.0-20181029044818-c44066c5c816 h1:mVFkLpejdFLXVUv9E42f3XJVfMdqd0IVLVIVLjZWn5o=
 golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35 h1:YAFjXN64LMvktoUZH9zgY4lGc/msGN7HQfoSuKCgaDU=
diff --git a/ssh/ssh.go b/ssh/ssh.go
new file mode 100644
index 0000000..b47d978
--- /dev/null
+++ b/ssh/ssh.go
@@ -0,0 +1,79 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+package ssh
+
+import (
+	"context"
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/x509"
+	"encoding/pem"
+	"fmt"
+	"time"
+
+	"fuchsia.googlesource.com/tools/retry"
+
+	"golang.org/x/crypto/ssh"
+)
+
+const (
+	// Default SSH server port.
+	Port = 22
+
+	// Default RSA key size.
+	KeySize = 2048
+)
+
+// GenerateKeyPair generates a pair of private/public keys.
+func GenerateKeyPairt(bitSize int) ([]byte, []byte, error) {
+	key, err := rsa.GenerateKey(rand.Reader, bitSize)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	pubkey, err := ssh.NewPublicKey(&key.PublicKey)
+	if err != nil {
+		return nil, nil, err
+	}
+	pembuf := pubkey.Marshal()
+
+	var privateKey = &pem.Block{
+		Type:  "RSA PRIVATE KEY",
+		Bytes: x509.MarshalPKCS1PrivateKey(key),
+	}
+	buf := pem.EncodeToMemory(privateKey)
+
+	return pembuf, buf, nil
+}
+
+// Connect establishes a new SSH connection to a server with the given
+// address and port, using the provided user name and private key.
+func Connect(ctx context.Context, address string, port int, user string, privateKey []byte) (*ssh.Client, error) {
+	signer, err := ssh.ParsePrivateKey(privateKey)
+	if err != nil {
+		return nil, fmt.Errorf("cannot parse the private key: %v", err)
+	}
+
+	config := &ssh.ClientConfig{
+		User: user,
+		Auth: []ssh.AuthMethod{
+			ssh.PublicKeys(signer),
+		},
+		Timeout:         time.Minute, // TODO: allow passing the timeout
+		HostKeyCallback: ssh.InsecureIgnoreHostKey(),
+	}
+
+	var client *ssh.Client
+	addr := fmt.Sprintf("%s:%d", address, port)
+	// TODO: figure out optimal backoff time and number of retries
+	err = retry.Retry(ctx, retry.WithMaxRetries(retry.NewConstantBackoff(time.Second), 10), func() error {
+		client, err = ssh.Dial("tcp", addr, config)
+		return err
+	})
+	if err != nil {
+		return nil, fmt.Errorf("cannot connect to \"%s\": %v", addr, err)
+	}
+
+	return client, nil
+}