From 3f81d366de59ff1783c298ae228b5940dd4bce40 Mon Sep 17 00:00:00 2001 From: Jarek Kowalski Date: Sat, 21 May 2016 15:15:38 -0700 Subject: [PATCH] added genpasswd command --- Makefile | 2 +- cmd/kopia/command_create.go | 21 +++-- cmd/kopia/command_genpasswd.go | 165 +++++++++++++++++++++++++++++++++ cmd/kopia/config.go | 3 +- cmd/kopia/main.go | 13 +-- doc/tutorial.md | 4 +- vault/creds.go | 2 + vault/vault.go | 92 ++++++++++-------- vfs/manager.go | 2 + 9 files changed, 238 insertions(+), 66 deletions(-) create mode 100644 cmd/kopia/command_genpasswd.go diff --git a/Makefile b/Makefile index 75111407a..d7c93899e 100644 --- a/Makefile +++ b/Makefile @@ -30,7 +30,7 @@ test: vtest: go test -v -timeout 30s github.com/kopia/kopia/... -doc: +godoc: godoc -http=:33333 coverage: diff --git a/cmd/kopia/command_create.go b/cmd/kopia/command_create.go index e03ad7ccc..6b92864dc 100644 --- a/cmd/kopia/command_create.go +++ b/cmd/kopia/command_create.go @@ -5,21 +5,21 @@ "fmt" "io" - "github.com/kopia/kopia/repo" - "github.com/kopia/kopia/vault" + "gopkg.in/alecthomas/kingpin.v2" "github.com/kopia/kopia/blob" - "gopkg.in/alecthomas/kingpin.v2" + "github.com/kopia/kopia/repo" + "github.com/kopia/kopia/vault" ) var ( createCommand = app.Command("create", "Create new vault and repository.") createCommandRepository = createCommand.Flag("repository", "Repository path.").Required().String() - createObjectFormat = createCommand.Flag("repo-format", "Format of repository objects.").PlaceHolder("FORMAT").Default("sha256t128-aes256").Enum(supportedObjectFormats()...) + createObjectFormat = createCommand.Flag("repo-format", "Format of repository objects.").PlaceHolder("FORMAT").Default("sha256t160-aes192").Enum(supportedObjectFormats()...) createMaxBlobSize = createCommand.Flag("max-blob-size", "Maximum size of a data chunk.").PlaceHolder("BYTES").Default("20000000").Int() createInlineBlobSize = createCommand.Flag("inline-blob-size", "Maximum size of an inline data chunk.").PlaceHolder("BYTES").Default("32768").Int() - createVaultEncryptionFormat = createCommand.Flag("vault-format", "Vault encryption format.").PlaceHolder("FORMAT").Default("aes-256").Enum(supportedVaultEncryptionFormats()...) + createVaultEncryptionFormat = createCommand.Flag("vault-encryption", "Vault encryption.").PlaceHolder("FORMAT").Default("aes-256").Enum(supportedVaultEncryptionFormats()...) createOverwrite = createCommand.Flag("overwrite", "Overwrite existing data (DANGEROUS).").Bool() createOnly = createCommand.Flag("create-only", "Create the vault, but don't connect to it.").Short('c').Bool() ) @@ -100,16 +100,16 @@ func runCreateCommand(context *kingpin.ParseContext) error { repoFormat.ObjectFormat, repoFormat.MaxBlobSize) - creds, err := getVaultCredentials(true) - if err != nil { - return fmt.Errorf("unable to get credentials: %v", err) - } - vf, err := vaultFormat() if err != nil { return fmt.Errorf("unable to initialize vault format: %v", err) } + creds, err := getVaultCredentials(true) + if err != nil { + return fmt.Errorf("unable to get credentials: %v", err) + } + fmt.Printf( "Initializing vault in '%s' with encryption '%v'.\n", vaultStorage.Configuration().Config.ToURL().String(), @@ -147,6 +147,7 @@ func supportedVaultEncryptionFormats() []string { return []string{ "none", "aes-128", + "aes-192", "aes-256", } } diff --git a/cmd/kopia/command_genpasswd.go b/cmd/kopia/command_genpasswd.go new file mode 100644 index 000000000..7626528a9 --- /dev/null +++ b/cmd/kopia/command_genpasswd.go @@ -0,0 +1,165 @@ +package main + +import ( + "bytes" + "crypto/rand" + "fmt" + "io" + "io/ioutil" + "math" + "net/http" + "os" + "strings" + + "gopkg.in/alecthomas/kingpin.v2" +) + +var ( + genPasswordCommand = app.Command("genpasswd", "Generate memorable password - inspired by http://xkcd.com/936/") + genPasswordWordsSource = genPasswordCommand.Flag("word-list", "Dictionary words URL or file name").Default("http://kopia.github.io/words/en.txt").String() + genPasswordWordsPerPassword = genPasswordCommand.Flag("words-per-password", "Number of words per password.").Short('w').Default("6").Int() + genPasswordWordSeparator = genPasswordCommand.Flag("words-separator", "Word separator.").Default("-").Short('s').String() + genPasswordUppercase = genPasswordCommand.Flag("uppercase", "Use upper-case versions of words.").Short('u').Bool() + genPasswordCapitalize = genPasswordCommand.Flag("capitalize", "Use capitalized versions of words.").Short('c').Bool() + genPasswordL33t = genPasswordCommand.Flag("l33t", "Use common substitutions o->0, s->$, etc.").Short('l').Bool() + genPasswordMinWordLength = genPasswordCommand.Flag("min-word-length", "Minimum dictionary word length.").Default("4").Int() + genPasswordMaxWordLength = genPasswordCommand.Flag("max-word-length", "Maximum dictionary word length.").Default("8").Int() + genPasswordCount = genPasswordCommand.Flag("num-passwords", "Number of passwords to generate.").Short('n').Default("20").Int() + + l33tSubstTable = map[string]string{ + "a": "4", + "e": "3", + "l": "1", + "s": "$", + "o": "0", + } +) + +func init() { + genPasswordCommand.Action(runGenPassword) +} + +func genleet(result *[]string, prefix string, suffix string) { + if len(suffix) == 0 { + return + } + + *result = append(*result, prefix+suffix) + + ch := suffix[0:1] + genleet(result, prefix+ch, suffix[1:]) + if subst, ok := l33tSubstTable[ch]; ok { + genleet(result, prefix+subst, suffix[1:]) + } +} + +func l33t(w string) []string { + var result []string + + genleet(&result, "", w) + + return result +} + +func getWordList() ([]string, error) { + // Read the words file, that is typically 2-5MB, not a big deal. + allWords, err := ioutil.ReadFile(*genPasswordWordsSource) + if err != nil { + if os.IsNotExist(err) { + // File not found, try URL. + fmt.Printf("Downloading word list from %v ...\n", *genPasswordWordsSource) + resp, err := http.Get(*genPasswordWordsSource) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + allWords, err = ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + } else { + return nil, err + } + } else { + fmt.Printf("Using word list from file: %v\n", *genPasswordWordsSource) + } + + words := bytes.Split(allWords, []byte("\n")) + var usableWords []string + + for _, w := range words { + word := strings.TrimSpace(string(w)) + if len(word) >= *genPasswordMinWordLength && len(word) <= *genPasswordMaxWordLength { + usableWords = append(usableWords, word) + if *genPasswordUppercase { + usableWords = append(usableWords, strings.ToUpper(word)) + } + if *genPasswordCapitalize { + usableWords = append(usableWords, strings.ToUpper(word[0:1])+word[1:]) + } + if *genPasswordL33t { + usableWords = append(usableWords, l33t(word)...) + } + } + } + + if len(usableWords) < 500 { + return nil, fmt.Errorf("word list too short: %v entries", len(usableWords)) + } + fmt.Printf("Got %v usable words.\n", len(usableWords)) + fmt.Printf("\n") + + return usableWords, nil +} + +func runGenPassword(context *kingpin.ParseContext) error { + usableWords, err := getWordList() + if err != nil { + return fmt.Errorf("unable to read word list: %v", err) + } + + randomBitsPerWord := math.Log2(float64(len(usableWords))) + randomBitsPerSeparator := math.Log2(float64(len(*genPasswordWordSeparator))) + wordCharSetSize := 26 + if *genPasswordUppercase || *genPasswordCapitalize { + wordCharSetSize *= 2 + } + + fmt.Printf("Memorable passwords, inspired by http://xkcd.com/936/ :\n") + fmt.Printf("\n") + + for i := 0; i < *genPasswordCount; i++ { + pass := generatePasswordFromWords(usableWords) + + fmt.Printf("%2d. %-60v blind entropy %.2f bits\n", i+1, pass, math.Log2(math.Pow(float64(wordCharSetSize), float64(len(pass))))) + } + + fmt.Printf("\n") + fmt.Printf("Password entropy with full knowledge of the algorithm: %.2f bits.\n", + randomBitsPerWord*float64(*genPasswordWordsPerPassword)+ + randomBitsPerSeparator*float64(len(*genPasswordWordSeparator))) + + return nil +} + +func secureRandomInt() int { + var b [4]byte + io.ReadFull(rand.Reader, b[:]) + return ((int(b[0]) & 0x7F) << 24) | (int(b[1]) << 16) | (int(b[2]) << 8) | int(b[3]) +} + +func generatePasswordFromWords(words []string) string { + var result string + separators := *genPasswordWordSeparator + + for i := 0; i < *genPasswordWordsPerPassword; i++ { + if i > 0 { + x := secureRandomInt() % len(separators) + result += separators[x : x+1] + } + result += words[secureRandomInt()%len(words)] + } + + return result +} diff --git a/cmd/kopia/config.go b/cmd/kopia/config.go index f3b4d43b1..2dad02b65 100644 --- a/cmd/kopia/config.go +++ b/cmd/kopia/config.go @@ -10,10 +10,9 @@ "strings" "github.com/kopia/kopia/blob" + "github.com/kopia/kopia/vault" "golang.org/x/crypto/ssh/terminal" - - "github.com/kopia/kopia/vault" ) var ( diff --git a/cmd/kopia/main.go b/cmd/kopia/main.go index 28d4a7e8f..e5616df7b 100644 --- a/cmd/kopia/main.go +++ b/cmd/kopia/main.go @@ -1,21 +1,10 @@ /* -The 'kopia' utility support screating and accessing backups from command line. +The 'kopia' utility supports creating and accessing backups from command line. Usage: $ kopia [] [ ...] -Common subcommands: - - init [ ...] - Connects to the backup repo. - - backup [] ... - Copies local directory to backup repository. - - mount --objectID=CHUNKID - Mounts remote backup as local directory. - Use 'kopia help' to see more details. */ package main diff --git a/doc/tutorial.md b/doc/tutorial.md index fdc9ce07c..acf2f32b5 100644 --- a/doc/tutorial.md +++ b/doc/tutorial.md @@ -13,7 +13,7 @@ You can download pre-built `kopia` binary from http://kopia.github.io/download. ### Installation From Source -To build Kopia from source you need to have the latest version of [Go](https://golang.org/dl/) installed and run the following commands: +To build Kopia from source you need the latest version of [Go](https://golang.org/dl/) and run the following commands: ``` mkdir $HOME/kopia @@ -22,7 +22,7 @@ go get github.com/kopia/kopia go install github.com/kopia/kopia/cmd/kopia ``` -This will automatically download and build kopia and put the resulting binary in `$HOME/kopia/bin`. For convenience it's best to add this directory to system `PATH` or copy it to a directory already in the path, such as `/usr/local/bin`. +This will automatically download and build kopia and put the resulting binary in `$HOME/kopia/bin`. For convenience it's best to add this directory to system `PATH` or copy/symlink it to a directory already in the path, such as `/usr/local/bin`. ## Getting Started diff --git a/vault/creds.go b/vault/creds.go index e514f4c03..beeb50d1e 100644 --- a/vault/creds.go +++ b/vault/creds.go @@ -32,6 +32,7 @@ func (mkc *masterKeyCredentials) getMasterKey(salt []byte) []byte { return mkc.key } +// MasterKey returns master key-based Credentials with the specified key. func MasterKey(key []byte) (Credentials, error) { if len(key) < MinMasterKeyLength { return nil, fmt.Errorf("master key too short") @@ -48,6 +49,7 @@ func (pc *passwordCredentials) getMasterKey(salt []byte) []byte { return pbkdf2.Key([]byte(pc.password), salt, pbkdf2Rounds, passwordBasedKeySize, sha256.New) } +// Password returns password-based Credentials with the specified password. func Password(password string) (Credentials, error) { if len(password) < MinPasswordLength { return nil, fmt.Errorf("password too short") diff --git a/vault/vault.go b/vault/vault.go index d61eb7769..d3cd1130f 100644 --- a/vault/vault.go +++ b/vault/vault.go @@ -44,63 +44,73 @@ func (v *Vault) writeEncryptedBlock(name string, content []byte) error { return err } - hash, err := v.newChecksum() - if err != nil { - return err + if blk != nil { + hash, err := v.newChecksum() + if err != nil { + return err + } + + ivLength := blk.BlockSize() + ivPlusContentLength := ivLength + len(content) + cipherText := make([]byte, ivPlusContentLength+hash.Size()) + + // Store IV at the beginning of ciphertext. + iv := cipherText[0:ivLength] + if _, err := io.ReadFull(rand.Reader, iv); err != nil { + return err + } + + ctr := cipher.NewCTR(blk, iv) + ctr.XORKeyStream(cipherText[ivLength:], content) + hash.Write(cipherText[0:ivPlusContentLength]) + copy(cipherText[ivPlusContentLength:], hash.Sum(nil)) + + content = cipherText } - ivLength := blk.BlockSize() - ivPlusContentLength := ivLength + len(content) - cipherText := make([]byte, ivPlusContentLength+hash.Size()) - - // Store IV at the beginning of ciphertext. - iv := cipherText[0:ivLength] - if _, err := io.ReadFull(rand.Reader, iv); err != nil { - return err - } - - ctr := cipher.NewCTR(blk, iv) - ctr.XORKeyStream(cipherText[ivLength:], content) - hash.Write(cipherText[0:ivPlusContentLength]) - copy(cipherText[ivPlusContentLength:], hash.Sum(nil)) - - return v.storage.PutBlock(name, ioutil.NopCloser(bytes.NewBuffer(cipherText)), blob.PutOptions{ + return v.storage.PutBlock(name, ioutil.NopCloser(bytes.NewBuffer(content)), blob.PutOptions{ Overwrite: true, }) } func (v *Vault) readEncryptedBlock(name string) ([]byte, error) { - cipherText, err := v.storage.GetBlock(name) + content, err := v.storage.GetBlock(name) if err != nil { return nil, err } - hash, err := v.newChecksum() - if err != nil { - return nil, err - } - - p := len(cipherText) - hash.Size() - hash.Write(cipherText[0:p]) - expectedChecksum := hash.Sum(nil) - actualChecksum := cipherText[p:] - if !hmac.Equal(expectedChecksum, actualChecksum) { - return nil, fmt.Errorf("cannot read encrypted block: incorrect checksum") - } - blk, err := v.newCipher() if err != nil { return nil, err } - ivLength := blk.BlockSize() + if blk != nil { - plainText := make([]byte, len(cipherText)-ivLength-hash.Size()) - iv := cipherText[0:blk.BlockSize()] + hash, err := v.newChecksum() + if err != nil { + return nil, err + } - ctr := cipher.NewCTR(blk, iv) - ctr.XORKeyStream(plainText, cipherText[ivLength:len(cipherText)-hash.Size()]) - return plainText, nil + p := len(content) - hash.Size() + hash.Write(content[0:p]) + expectedChecksum := hash.Sum(nil) + actualChecksum := content[p:] + if !hmac.Equal(expectedChecksum, actualChecksum) { + return nil, fmt.Errorf("cannot read encrypted block: incorrect checksum") + } + + ivLength := blk.BlockSize() + + plainText := make([]byte, len(content)-ivLength-hash.Size()) + iv := content[0:blk.BlockSize()] + + ctr := cipher.NewCTR(blk, iv) + ctr.XORKeyStream(plainText, content[ivLength:len(content)-hash.Size()]) + + content = plainText + } + + return content, nil } func (v *Vault) newChecksum() (hash.Hash, error) { @@ -118,6 +128,8 @@ func (v *Vault) newChecksum() (hash.Hash, error) { func (v *Vault) newCipher() (cipher.Block, error) { switch v.format.Encryption { + case "none": + return nil, nil case "aes-128": k := make([]byte, 16) v.deriveKey(purposeAESKey, k) @@ -181,6 +193,7 @@ func (v *Vault) OpenRepository() (repo.Repository, error) { return repo.NewRepository(storage, rc.Format) } +// Get deserializes JSON data stored in the vault into the specified content structure. func (v *Vault) Get(id string, content interface{}) error { j, err := v.readEncryptedBlock(id) if err != nil { @@ -190,6 +203,7 @@ func (v *Vault) Get(id string, content interface{}) error { return json.Unmarshal(j, content) } +// Put stores the contents of an item stored in a vault with a given ID. func (v *Vault) Put(id string, content interface{}) error { j, err := json.Marshal(content) if err != nil { diff --git a/vfs/manager.go b/vfs/manager.go index c7564ceef..cc06bc258 100644 --- a/vfs/manager.go +++ b/vfs/manager.go @@ -10,6 +10,7 @@ "github.com/kopia/kopia/repo" ) +// Manager exposes FUSE filesystem nodes based on repository entries. type Manager interface { NewNodeFromEntry(e *fs.Entry) fusefs.Node } @@ -50,6 +51,7 @@ func (mgr *manager) open(oid repo.ObjectID) (io.ReadSeeker, error) { return mgr.repo.Open(oid) } +// NewManager returns new vfs.Manager that func NewManager(repo repo.Repository) Manager { return &manager{ repo: repo,