github.com/avahowell/sia@v0.5.1-beta.0.20160524050156-83dcc3d37c94/modules/wallet/seed.go (about)

     1  package wallet
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/rand"
     6  	"errors"
     7  	"path/filepath"
     8  
     9  	"github.com/NebulousLabs/Sia/build"
    10  	"github.com/NebulousLabs/Sia/crypto"
    11  	"github.com/NebulousLabs/Sia/modules"
    12  	"github.com/NebulousLabs/Sia/persist"
    13  	"github.com/NebulousLabs/Sia/types"
    14  )
    15  
    16  const (
    17  	seedFilePrefix = "Sia Wallet Encrypted Backup Seed - "
    18  	seedFileSuffix = ".seed"
    19  )
    20  
    21  var (
    22  	errAddressExhaustion = errors.New("current seed has used all available addresses")
    23  	errKnownSeed         = errors.New("seed is already known")
    24  )
    25  
    26  type (
    27  	// UniqueID is a unique id randomly generated and put at the front of every
    28  	// persistence object. It is used to make sure that a different encryption
    29  	// key can be used for every persistence object.
    30  	UniqueID [crypto.EntropySize]byte
    31  
    32  	// SeedFile stores an encrypted wallet seed on disk.
    33  	SeedFile struct {
    34  		UID                    UniqueID
    35  		EncryptionVerification crypto.Ciphertext
    36  		Seed                   crypto.Ciphertext
    37  	}
    38  )
    39  
    40  // generateUnlockConditions provides the unlock conditions that would be
    41  // automatically generated from the input public key.
    42  func generateUnlockConditions(pk crypto.PublicKey) types.UnlockConditions {
    43  	return types.UnlockConditions{
    44  		PublicKeys: []types.SiaPublicKey{{
    45  			Algorithm: types.SignatureEd25519,
    46  			Key:       pk[:],
    47  		}},
    48  		SignaturesRequired: 1,
    49  	}
    50  }
    51  
    52  // generateSpendableKey creates the keys and unlock conditions a given index of a
    53  // seed.
    54  func generateSpendableKey(seed modules.Seed, index uint64) spendableKey {
    55  	// Generate the keys and unlock conditions.
    56  	entropy := crypto.HashAll(seed, index)
    57  	sk, pk := crypto.GenerateKeyPairDeterministic(entropy)
    58  	return spendableKey{
    59  		UnlockConditions: generateUnlockConditions(pk),
    60  		SecretKeys:       []crypto.SecretKey{sk},
    61  	}
    62  }
    63  
    64  // encryptAndSaveSeedFile encrypts and saves a seed file.
    65  func (w *Wallet) encryptAndSaveSeedFile(masterKey crypto.TwofishKey, seed modules.Seed) (SeedFile, error) {
    66  	var sf SeedFile
    67  	_, err := rand.Read(sf.UID[:])
    68  	if err != nil {
    69  		return SeedFile{}, err
    70  	}
    71  	sek := uidEncryptionKey(masterKey, sf.UID)
    72  	plaintextVerification := make([]byte, encryptionVerificationLen)
    73  	sf.EncryptionVerification, err = sek.EncryptBytes(plaintextVerification)
    74  	if err != nil {
    75  		return SeedFile{}, err
    76  	}
    77  	sf.Seed, err = sek.EncryptBytes(seed[:])
    78  	if err != nil {
    79  		return SeedFile{}, err
    80  	}
    81  	seedFilename := filepath.Join(w.persistDir, seedFilePrefix+persist.RandomSuffix()+seedFileSuffix)
    82  	err = persist.SaveFileSync(seedMetadata, sf, seedFilename)
    83  	if err != nil {
    84  		return SeedFile{}, err
    85  	}
    86  	return sf, nil
    87  }
    88  
    89  // decryptSeedFile decrypts a seed file using the encryption key.
    90  func decryptSeedFile(masterKey crypto.TwofishKey, sf SeedFile) (seed modules.Seed, err error) {
    91  	// Verify that the provided master key is the correct key.
    92  	decryptionKey := uidEncryptionKey(masterKey, sf.UID)
    93  	expectedDecryptedVerification := make([]byte, crypto.EntropySize)
    94  	decryptedVerification, err := decryptionKey.DecryptBytes(sf.EncryptionVerification)
    95  	if err != nil {
    96  		return modules.Seed{}, err
    97  	}
    98  	if !bytes.Equal(expectedDecryptedVerification, decryptedVerification) {
    99  		return modules.Seed{}, modules.ErrBadEncryptionKey
   100  	}
   101  
   102  	// Decrypt and return the seed.
   103  	plainSeed, err := decryptionKey.DecryptBytes(sf.Seed)
   104  	if err != nil {
   105  		return modules.Seed{}, err
   106  	}
   107  	copy(seed[:], plainSeed)
   108  	return seed, nil
   109  }
   110  
   111  // integrateSeed takes an address seed as input and from that generates
   112  // 'publicKeysPerSeed' addresses that the wallet is able to spend.
   113  // integrateSeed should not be called with the primary seed.
   114  func (w *Wallet) integrateSeed(seed modules.Seed) {
   115  	for i := uint64(0); i < modules.PublicKeysPerSeed; i++ {
   116  		// Generate the key and check it is new to the wallet.
   117  		spendableKey := generateSpendableKey(seed, i)
   118  		w.keys[spendableKey.UnlockConditions.UnlockHash()] = spendableKey
   119  	}
   120  	w.seeds = append(w.seeds, seed)
   121  }
   122  
   123  // recoverSeed integrates a recovery seed into the wallet.
   124  func (w *Wallet) recoverSeed(masterKey crypto.TwofishKey, seed modules.Seed) error {
   125  	// Because the recovery seed does not have a UID, duplication must be
   126  	// prevented by comparing with the list of decrypted seeds. This can only
   127  	// occur while the wallet is unlocked.
   128  	if !w.unlocked {
   129  		return modules.ErrLockedWallet
   130  	}
   131  
   132  	// Check that the seed is not already known.
   133  	for _, wSeed := range w.seeds {
   134  		if seed == wSeed {
   135  			return errKnownSeed
   136  		}
   137  	}
   138  	if seed == w.primarySeed {
   139  		return errKnownSeed
   140  	}
   141  	seedFile, err := w.encryptAndSaveSeedFile(masterKey, seed)
   142  	if err != nil {
   143  		return err
   144  	}
   145  
   146  	// Add the seed file to the wallet's set of tracked seeds and save the
   147  	// wallet settings.
   148  	w.persist.AuxiliarySeedFiles = append(w.persist.AuxiliarySeedFiles, seedFile)
   149  	err = w.saveSettingsSync()
   150  	if err != nil {
   151  		return err
   152  	}
   153  	w.integrateSeed(seed)
   154  	return nil
   155  
   156  }
   157  
   158  // createSeed creates a wallet seed and encrypts it using a key derived from
   159  // the master key, then addds it to the wallet as the primary seed, while
   160  // making a disk backup.
   161  func (w *Wallet) createSeed(masterKey crypto.TwofishKey, seed modules.Seed) error {
   162  	seedFile, err := w.encryptAndSaveSeedFile(masterKey, seed)
   163  	if err != nil {
   164  		return err
   165  	}
   166  	w.primarySeed = seed
   167  	w.persist.PrimarySeedFile = seedFile
   168  	w.persist.PrimarySeedProgress = 0
   169  	// The wallet preloads keys to prevent confusion for people using the same
   170  	// seed/wallet file in multiple places.
   171  	for i := uint64(0); i < modules.WalletSeedPreloadDepth; i++ {
   172  		spendableKey := generateSpendableKey(seed, i)
   173  		w.keys[spendableKey.UnlockConditions.UnlockHash()] = spendableKey
   174  	}
   175  	return w.saveSettingsSync()
   176  }
   177  
   178  // initPrimarySeed loads the primary seed into the wallet.
   179  func (w *Wallet) initPrimarySeed(masterKey crypto.TwofishKey) error {
   180  	seed, err := decryptSeedFile(masterKey, w.persist.PrimarySeedFile)
   181  	if err != nil {
   182  		return err
   183  	}
   184  	// The wallet preloads keys to prevent confusion when using the same wallet
   185  	// in multiple places.
   186  	for i := uint64(0); i < w.persist.PrimarySeedProgress+modules.WalletSeedPreloadDepth; i++ {
   187  		spendableKey := generateSpendableKey(seed, i)
   188  		w.keys[spendableKey.UnlockConditions.UnlockHash()] = spendableKey
   189  	}
   190  	w.primarySeed = seed
   191  	w.seeds = append(w.seeds, seed)
   192  	return nil
   193  }
   194  
   195  // initAuxiliarySeeds scans the wallet folder for wallet seeds.
   196  func (w *Wallet) initAuxiliarySeeds(masterKey crypto.TwofishKey) error {
   197  	for _, seedFile := range w.persist.AuxiliarySeedFiles {
   198  		seed, err := decryptSeedFile(masterKey, seedFile)
   199  		if build.DEBUG && err != nil {
   200  			panic(err)
   201  		}
   202  		if err != nil {
   203  			w.log.Println("UNLOCK: failed to load an auxiliary seed:", err)
   204  			continue
   205  		}
   206  		w.integrateSeed(seed)
   207  	}
   208  	return nil
   209  }
   210  
   211  // nextPrimarySeedAddress fetches the next address from the primary seed.
   212  func (w *Wallet) nextPrimarySeedAddress() (types.UnlockConditions, error) {
   213  	// Check that the wallet has been unlocked.
   214  	if !w.unlocked {
   215  		return types.UnlockConditions{}, modules.ErrLockedWallet
   216  	}
   217  
   218  	// Integrate the next key into the wallet, and return the unlock
   219  	// conditions. Because the wallet preloads keys, the progress used is
   220  	// 'PrimarySeedProgress+modules.WalletSeedPreloadDepth'.
   221  	spendableKey := generateSpendableKey(w.primarySeed, w.persist.PrimarySeedProgress+modules.WalletSeedPreloadDepth)
   222  	w.keys[spendableKey.UnlockConditions.UnlockHash()] = spendableKey
   223  	w.persist.PrimarySeedProgress++
   224  	err := w.saveSettingsSync()
   225  	if err != nil {
   226  		return types.UnlockConditions{}, err
   227  	}
   228  	return spendableKey.UnlockConditions, nil
   229  }
   230  
   231  // AllSeeds returns a list of all seeds known to and used by the wallet.
   232  func (w *Wallet) AllSeeds() ([]modules.Seed, error) {
   233  	w.mu.Lock()
   234  	defer w.mu.Unlock()
   235  	if !w.unlocked {
   236  		return nil, modules.ErrLockedWallet
   237  	}
   238  	return w.seeds, nil
   239  }
   240  
   241  // PrimarySeed returns the decrypted primary seed of the wallet.
   242  func (w *Wallet) PrimarySeed() (modules.Seed, uint64, error) {
   243  	w.mu.Lock()
   244  	defer w.mu.Unlock()
   245  	if !w.unlocked {
   246  		return modules.Seed{}, 0, modules.ErrLockedWallet
   247  	}
   248  	return w.primarySeed, w.persist.PrimarySeedProgress, nil
   249  }
   250  
   251  // NextAddress returns an unlock hash that is ready to receive siacoins or
   252  // siafunds. The address is generated using the primary address seed.
   253  func (w *Wallet) NextAddress() (types.UnlockConditions, error) {
   254  	w.mu.Lock()
   255  	defer w.mu.Unlock()
   256  	return w.nextPrimarySeedAddress()
   257  }
   258  
   259  // LoadSeed will track all of the addresses generated by the input seed,
   260  // reclaiming any funds that were lost due to a deleted file or lost encryption
   261  // key. An error will be returned if the seed has already been integrated with
   262  // the wallet.
   263  func (w *Wallet) LoadSeed(masterKey crypto.TwofishKey, seed modules.Seed) error {
   264  	w.mu.Lock()
   265  	defer w.mu.Unlock()
   266  	err := w.checkMasterKey(masterKey)
   267  	if err != nil {
   268  		return err
   269  	}
   270  	return w.recoverSeed(masterKey, seed)
   271  }