github.com/mattyw/juju@v0.0.0-20140610034352-732aecd63861/utils/ssh/clientkeys.go (about)

     1  // Copyright 2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package ssh
     5  
     6  import (
     7  	"fmt"
     8  	"io/ioutil"
     9  	"os"
    10  	"path/filepath"
    11  	"strings"
    12  	"sync"
    13  
    14  	"code.google.com/p/go.crypto/ssh"
    15  	"github.com/juju/utils"
    16  	"github.com/juju/utils/set"
    17  )
    18  
    19  const clientKeyName = "juju_id_rsa"
    20  
    21  // PublicKeySuffix is the file extension for public key files.
    22  const PublicKeySuffix = ".pub"
    23  
    24  var (
    25  	clientKeysMutex sync.Mutex
    26  
    27  	// clientKeys is a cached map of private key filenames
    28  	// to ssh.Signers. The private keys are those loaded
    29  	// from the client key directory, passed to LoadClientKeys.
    30  	clientKeys map[string]ssh.Signer
    31  )
    32  
    33  // LoadClientKeys loads the client SSH keys from the
    34  // specified directory, and caches them as a process-wide
    35  // global. If the directory does not exist, it is created;
    36  // if the directory did not exist, or contains no keys, it
    37  // is populated with a new key pair.
    38  //
    39  // If the directory exists, then all pairs of files where one
    40  // has the same name as the other + ".pub" will be loaded as
    41  // private/public key pairs.
    42  //
    43  // Calls to LoadClientKeys will clear the previously loaded
    44  // keys, and recompute the keys.
    45  func LoadClientKeys(dir string) error {
    46  	clientKeysMutex.Lock()
    47  	defer clientKeysMutex.Unlock()
    48  	dir, err := utils.NormalizePath(dir)
    49  	if err != nil {
    50  		return err
    51  	}
    52  	if _, err := os.Stat(dir); err == nil {
    53  		keys, err := loadClientKeys(dir)
    54  		if err != nil {
    55  			return err
    56  		} else if len(keys) > 0 {
    57  			clientKeys = keys
    58  			return nil
    59  		}
    60  		// Directory exists but contains no keys;
    61  		// fall through and create one.
    62  	}
    63  	if err := os.MkdirAll(dir, 0700); err != nil {
    64  		return err
    65  	}
    66  	keyfile, key, err := generateClientKey(dir)
    67  	if err != nil {
    68  		os.RemoveAll(dir)
    69  		return err
    70  	}
    71  	clientKeys = map[string]ssh.Signer{keyfile: key}
    72  	return nil
    73  }
    74  
    75  // ClearClientKeys clears the client keys cached in memory.
    76  func ClearClientKeys() {
    77  	clientKeysMutex.Lock()
    78  	defer clientKeysMutex.Unlock()
    79  	clientKeys = nil
    80  }
    81  
    82  func generateClientKey(dir string) (keyfile string, key ssh.Signer, err error) {
    83  	private, public, err := GenerateKey("juju-client-key")
    84  	if err != nil {
    85  		return "", nil, err
    86  	}
    87  	clientPrivateKey, err := ssh.ParsePrivateKey([]byte(private))
    88  	if err != nil {
    89  		return "", nil, err
    90  	}
    91  	privkeyFilename := filepath.Join(dir, clientKeyName)
    92  	if err = ioutil.WriteFile(privkeyFilename, []byte(private), 0600); err != nil {
    93  		return "", nil, err
    94  	}
    95  	if err := ioutil.WriteFile(privkeyFilename+PublicKeySuffix, []byte(public), 0600); err != nil {
    96  		os.Remove(privkeyFilename)
    97  		return "", nil, err
    98  	}
    99  	return privkeyFilename, clientPrivateKey, nil
   100  }
   101  
   102  func loadClientKeys(dir string) (map[string]ssh.Signer, error) {
   103  	publicKeyFiles, err := publicKeyFiles(dir)
   104  	if err != nil {
   105  		return nil, err
   106  	}
   107  	keys := make(map[string]ssh.Signer, len(publicKeyFiles))
   108  	for _, filename := range publicKeyFiles {
   109  		filename = filename[:len(filename)-len(PublicKeySuffix)]
   110  		data, err := ioutil.ReadFile(filename)
   111  		if err != nil {
   112  			return nil, err
   113  		}
   114  		keys[filename], err = ssh.ParsePrivateKey(data)
   115  		if err != nil {
   116  			return nil, fmt.Errorf("parsing key file %q: %v", filename, err)
   117  		}
   118  	}
   119  	return keys, nil
   120  }
   121  
   122  // privateKeys returns the private keys loaded by LoadClientKeys.
   123  func privateKeys() (signers []ssh.Signer) {
   124  	clientKeysMutex.Lock()
   125  	defer clientKeysMutex.Unlock()
   126  	for _, key := range clientKeys {
   127  		signers = append(signers, key)
   128  	}
   129  	return signers
   130  }
   131  
   132  // PrivateKeyFiles returns the filenames of private SSH keys loaded by
   133  // LoadClientKeys.
   134  func PrivateKeyFiles() []string {
   135  	clientKeysMutex.Lock()
   136  	defer clientKeysMutex.Unlock()
   137  	keyfiles := make([]string, 0, len(clientKeys))
   138  	for f := range clientKeys {
   139  		keyfiles = append(keyfiles, f)
   140  	}
   141  	return keyfiles
   142  }
   143  
   144  // PublicKeyFiles returns the filenames of public SSH keys loaded by
   145  // LoadClientKeys.
   146  func PublicKeyFiles() []string {
   147  	privkeys := PrivateKeyFiles()
   148  	pubkeys := make([]string, len(privkeys))
   149  	for i, priv := range privkeys {
   150  		pubkeys[i] = priv + PublicKeySuffix
   151  	}
   152  	return pubkeys
   153  }
   154  
   155  // publicKeyFiles returns the filenames of public SSH keys
   156  // in the specified directory (all the files ending with .pub).
   157  func publicKeyFiles(clientKeysDir string) ([]string, error) {
   158  	if clientKeysDir == "" {
   159  		return nil, nil
   160  	}
   161  	var keys []string
   162  	dir, err := os.Open(clientKeysDir)
   163  	if err != nil {
   164  		return nil, err
   165  	}
   166  	names, err := dir.Readdirnames(-1)
   167  	dir.Close()
   168  	if err != nil {
   169  		return nil, err
   170  	}
   171  	candidates := set.NewStrings(names...)
   172  	for _, name := range names {
   173  		if !strings.HasSuffix(name, PublicKeySuffix) {
   174  			continue
   175  		}
   176  		// If the private key filename also exists, add the file.
   177  		priv := name[:len(name)-len(PublicKeySuffix)]
   178  		if candidates.Contains(priv) {
   179  			keys = append(keys, filepath.Join(dir.Name(), name))
   180  		}
   181  	}
   182  	return keys, nil
   183  }