mirror of
https://github.com/kopia/kopia.git
synced 2026-04-30 19:03:38 -04:00
added genpasswd command
This commit is contained in:
2
Makefile
2
Makefile
@@ -30,7 +30,7 @@ test:
|
||||
vtest:
|
||||
go test -v -timeout 30s github.com/kopia/kopia/...
|
||||
|
||||
doc:
|
||||
godoc:
|
||||
godoc -http=:33333
|
||||
|
||||
coverage:
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
|
||||
165
cmd/kopia/command_genpasswd.go
Normal file
165
cmd/kopia/command_genpasswd.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 [<flags>] <subcommand> [<args> ...]
|
||||
|
||||
Common subcommands:
|
||||
|
||||
init <provider> [<args> ...]
|
||||
Connects to the backup repo.
|
||||
|
||||
backup [<flags>] <directory>...
|
||||
Copies local directory to backup repository.
|
||||
|
||||
mount --objectID=CHUNKID <mountpoint>
|
||||
Mounts remote backup as local directory.
|
||||
|
||||
Use 'kopia help' to see more details.
|
||||
*/
|
||||
package main
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user