github.com/mit-dci/lit@v0.0.0-20221102210550-8c3d3b49f2ce/lnutil/keyfile.go (about)

     1  package lnutil
     2  
     3  import (
     4  	"crypto/rand"
     5  	"encoding/hex"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"os"
     9  	"strings"
    10  
    11  	"github.com/mit-dci/lit/logging"
    12  
    13  	"github.com/howeyc/gopass"
    14  	"golang.org/x/crypto/nacl/secretbox"
    15  	"golang.org/x/crypto/scrypt"
    16  )
    17  
    18  /* should switch to the codahale/chacha20poly1305 package.  Or whatever we're
    19  using for LNDC; otherwise it's 2 almost-the-same stream ciphers to import */
    20  
    21  // warning! look at those imports! crypto! hopefully this works!
    22  
    23  /* on-disk stored keys are 32bytes.  This is good for ed25519 private keys,
    24  for seeds for bip32, for individual secp256k1 priv keys, and so on.
    25  32 bytes is enough for anyone.
    26  If you want fewer bytes, put some zeroes at the end */
    27  
    28  // LoadKeyFromFileInteractive opens the file 'filename' and presents a
    29  // keyboard prompt for the passphrase to decrypt it.  It returns the
    30  // key if decryption works, or errors out.
    31  func LoadKeyFromFileInteractive(filename string) (*[32]byte, error) {
    32  	a, err := os.Stat(filename)
    33  	if err != nil {
    34  		return nil, err
    35  	}
    36  	if a.Size() < 80 { // there can't be a password...
    37  		return LoadKeyFromFileArg(filename, nil)
    38  	}
    39  	fmt.Printf("passphrase: ")
    40  	pass, err := gopass.GetPasswd()
    41  	if err != nil {
    42  		return nil, err
    43  	}
    44  	fmt.Printf("\n")
    45  	return LoadKeyFromFileArg(filename, pass)
    46  }
    47  
    48  // LoadKeyFromFileArg opens the file and returns the key.  If the key is
    49  // unencrypted it will ignore the password argument.
    50  func LoadKeyFromFileArg(filename string, pass []byte) (*[32]byte, error) {
    51  	priv32 := new([32]byte)
    52  	keyhex, err := ioutil.ReadFile(filename)
    53  	if err != nil {
    54  		return priv32, err
    55  	}
    56  	keyhex = []byte(strings.TrimSpace(string(keyhex)))
    57  	enckey, err := hex.DecodeString(string(keyhex))
    58  	if err != nil {
    59  		return priv32, err
    60  	}
    61  
    62  	if len(enckey) == 32 { // UNencrypted key, length 32
    63  		v, p := os.LookupEnv("LIT_KEYFILE_WARN")
    64  		if !p || (p && v != "0") {
    65  			logging.Warnf("WARNING!! Key file not encrypted!!\n")
    66  			logging.Warnf("Anyone who can read the key file can take everything!\n")
    67  			logging.Warnf("You should start over and use a good passphrase!\n")
    68  		}
    69  
    70  		copy(priv32[:], enckey[:])
    71  		return priv32, nil
    72  	}
    73  	// enckey should be 72 bytes.  24 for scrypt salt/box nonce,
    74  	// 16 for box auth
    75  	if len(enckey) != 72 {
    76  		return priv32, fmt.Errorf("Key length error for %s ", filename)
    77  	}
    78  	// enckey is actually encrypted, get derived key from pass and salt
    79  	// first extract salt
    80  	salt := new([24]byte)      // salt (also nonce for secretbox)
    81  	dk32 := new([32]byte)      // derived key array
    82  	copy(salt[:], enckey[:24]) // first 24 bytes are scrypt salt/box nonce
    83  
    84  	dk, err := scrypt.Key(pass, salt[:], 16384, 8, 1, 32) // derive key
    85  	if err != nil {
    86  		return priv32, err
    87  	}
    88  	copy(dk32[:], dk[:]) // copy into fixed size array
    89  
    90  	// nonce for secretbox is the same as scrypt salt.  Seems fine.  Really.
    91  	priv, worked := secretbox.Open(nil, enckey[24:], salt, dk32)
    92  	if worked != true {
    93  		return priv32, fmt.Errorf("Decryption failed for %s ", filename)
    94  	}
    95  	copy(priv32[:], priv[:]) //copy decrypted private key into array
    96  
    97  	priv = nil // this probably doesn't do anything but... eh why not
    98  	return priv32, nil
    99  }
   100  
   101  // saves a 32 byte key to file, prompting for passphrase.
   102  // if user enters empty passphrase (hits enter twice), will be saved
   103  // in the clear.
   104  func SaveKeyToFileInteractive(filename string, priv32 *[32]byte) error {
   105  	var match bool
   106  	var err error
   107  	var pass1, pass2 []byte
   108  	for match != true {
   109  		fmt.Printf("passphrase: ")
   110  		pass1, err = gopass.GetPasswd()
   111  		if err != nil {
   112  			return err
   113  		}
   114  		fmt.Printf("repeat passphrase: ")
   115  		pass2, err = gopass.GetPasswd()
   116  		if err != nil {
   117  			return err
   118  		}
   119  		if string(pass1) == string(pass2) {
   120  			match = true
   121  		} else {
   122  			fmt.Printf("user input error.  Try again gl hf dd.\n")
   123  		}
   124  	}
   125  	fmt.Printf("\n")
   126  	return SaveKeyToFileArg(filename, priv32, pass1)
   127  }
   128  
   129  // saves a 32 byte key to a file, encrypting with pass.
   130  // if pass is nil or zero length, doesn't encrypt and just saves in hex.
   131  func SaveKeyToFileArg(filename string, priv32 *[32]byte, pass []byte) error {
   132  	if len(pass) == 0 { // zero-length pass, save unencrypted
   133  		keyhex := fmt.Sprintf("%x\n", priv32[:])
   134  		err := ioutil.WriteFile(filename, []byte(keyhex), 0600)
   135  		if err != nil {
   136  			return err
   137  		}
   138  		logging.Warnf("WARNING!! Key file not encrypted!!\n")
   139  		logging.Warnf("Anyone who can read the key file can take everything!\n")
   140  		logging.Warnf("You should start over and use a good passphrase!\n")
   141  		logging.Warnf("Saved unencrypted key at %s\n", filename)
   142  		return nil
   143  	}
   144  
   145  	salt := new([24]byte) // salt for scrypt / nonce for secretbox
   146  	dk32 := new([32]byte) // derived key from scrypt
   147  
   148  	//get 24 random bytes for scrypt salt (and secretbox nonce)
   149  	_, err := rand.Read(salt[:])
   150  	if err != nil {
   151  		return err
   152  	}
   153  	// next use the pass and salt to make a 32-byte derived key
   154  	dk, err := scrypt.Key(pass, salt[:], 16384, 8, 1, 32)
   155  	if err != nil {
   156  		return err
   157  	}
   158  	copy(dk32[:], dk[:])
   159  
   160  	enckey := append(salt[:], secretbox.Seal(nil, priv32[:], salt, dk32)...)
   161  	//	enckey = append(salt, enckey...)
   162  	keyhex := fmt.Sprintf("%x\n", enckey)
   163  
   164  	err = ioutil.WriteFile(filename, []byte(keyhex), 0600)
   165  	if err != nil {
   166  		return err
   167  	}
   168  	logging.Infof("Wrote encrypted key to %s\n", filename)
   169  	return nil
   170  }
   171  
   172  // ReadKeyFile returns an 32 byte key from a file.
   173  // If there's no file there, it'll make one.  If there's a password needed,
   174  // it'll prompt for one.  One stop function.
   175  func ReadKeyFile(filename string) (*[32]byte, error) {
   176  	key32 := new([32]byte)
   177  	_, err := os.Stat(filename)
   178  	if err != nil {
   179  		if os.IsNotExist(err) {
   180  			// no key found, generate and save one
   181  			logging.Infof("No file %s, generating.\n", filename)
   182  
   183  			_, err := rand.Read(key32[:])
   184  			if err != nil {
   185  				return nil, err
   186  			}
   187  
   188  			err = SaveKeyToFileInteractive(filename, key32)
   189  			if err != nil {
   190  				return nil, err
   191  			}
   192  		} else {
   193  			// unknown error, crash
   194  			logging.Errorf("unknown error reading keyfile\n")
   195  			return nil, err
   196  		}
   197  	}
   198  	return LoadKeyFromFileInteractive(filename)
   199  }