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 }