Skip to content

Commit

Permalink
Initial release
Browse files Browse the repository at this point in the history
  • Loading branch information
ignatk committed Oct 27, 2016
0 parents commit 6ad4b61
Show file tree
Hide file tree
Showing 9 changed files with 1,041 additions and 0 deletions.
11 changes: 11 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Copyright (c) 2016, Cloudflare. All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# gokey

## A simple vaultless password manager in Go
**gokey** is a password manager, which does not require a password vault. Instead of storing your passwords in a vault it derives your password on the fly from your master password and supplied _realm_ string (for example, resource URL). This way you do not have to manage, backup or sync your password vault (or trust its management to a third party) as your passwords are available immediately anywhere.

#### example
```
gokey -p super-secret-master-password -r example.com
```

145 changes: 145 additions & 0 deletions cmd/gokey/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// Command gokey is a vaultless password and key manager.
package main

import (
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"os"

"github.com/cloudflare/gokey"
)

var (
pass, keyType, seedPath, realm, output string
unsafe bool
seedSkipCount int
)

func init() {
flag.StringVar(&pass, "p", "", "master password")
flag.StringVar(&keyType, "t", "pass", "output type (can be pass, seed, raw, ec256, ec521, rsa2048, rsa4096)")
flag.StringVar(&seedPath, "s", "", "path to master seed file (optional)")
flag.IntVar(&seedSkipCount, "skip", 0, "number of bytes to skip from master seed file (default 0)")
flag.StringVar(&realm, "r", "", "password/key realm (most probably purpose of the password/key)")
flag.StringVar(&output, "o", "", "output path to store generated key/password (default stdout)")
flag.BoolVar(&unsafe, "u", false, "UNSAFE: allow key generation without a seed")
}

var keyTypes = map[string]int{
"ec256": gokey.KEYTYPE_EC256,
"ec521": gokey.KEYTYPE_EC521,
"rsa2048": gokey.KEYTYPE_RSA2048,
"rsa4096": gokey.KEYTYPE_RSA4096,
}

func genSeed(w io.Writer) {
seed, err := gokey.GenerateEncryptedKeySeed(pass)
if err != nil {
log.Fatalln(err)
}

_, err = w.Write(seed)
if err != nil {
log.Fatalln(err)
}
}

func genPass(seed []byte, w io.Writer) {
password, err := gokey.GetPass(pass, realm, seed, &gokey.PasswordSpec{10, 3, 3, 1, 1, ""})
if err != nil {
log.Fatalln(err)
}

_, err = io.WriteString(w, password)
if err != nil {
log.Fatalln(err)
}

fmt.Fprintln(w, "")
}

func genKey(seed []byte, w io.Writer) {
key, err := gokey.GetKey(pass, realm, seed, keyTypes[keyType], unsafe)
if err != nil {
log.Fatalln(err)
}

err = gokey.EncodeToPem(key, w)
if err != nil {
log.Fatalln(err)
}
}

// TODO: parametrize size
// generates raw 32 bytes
func genRaw(seed []byte, w io.Writer) {
raw, err := gokey.GetRaw(pass, realm, seed, unsafe)
if err != nil {
log.Fatalln(err)
}

_, err = io.CopyN(w, raw, 32)
if err != nil {
log.Fatalln(err)
}
}

func logFatal(format string, args ...interface{}) {
log.Printf(format, args...)
flag.PrintDefaults()
os.Exit(1)
}

func main() {
flag.Parse()

if pass == "" {
logFatal("no password provided")
}

out := os.Stdout
var err error
if output != "" {
out, err = os.OpenFile(output, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
log.Fatalln(err)
}
defer out.Close()
}

if keyType == "seed" {
genSeed(out)
} else {
if realm == "" {
logFatal("no realm provided")
}

var seed []byte
if seedPath != "" {
seed, err = ioutil.ReadFile(seedPath)
if err != nil {
log.Fatalln(err)
}

if (seedSkipCount < 0) || (seedSkipCount >= len(seed)) {
log.Fatalln("invalid skip parameter")
}
seed = seed[seedSkipCount:]
}

switch keyType {
case "pass":
genPass(seed, out)
case "raw":
genRaw(seed, out)
default:
if _, ok := keyTypes[keyType]; !ok {
logFatal("unknown key type: %v", keyType)
}
genKey(seed, out)
}
}
}
195 changes: 195 additions & 0 deletions csprng.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
package gokey

// we are using Fortuna Generator as our "reproducible" CSPRNG
// this implementation is simplified as we need only a repeatable PRNG and we seed it only once
// also, every instance of this PRNG will be used to generate only one password/key

import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"errors"
"io"

"golang.org/x/crypto/hkdf"
"golang.org/x/crypto/pbkdf2"
)

const (
keySeedLength = 256
)

type fortunaGenerator struct {
key []byte
counter [16]byte
cipher cipher.Block
buffer *bytes.Buffer
}

func (g *fortunaGenerator) increment() {
// from cipher/ctr/ctr.go (changed byte order)
for i := 0; i < len(g.counter); i++ {
g.counter[i]++
if g.counter[i] != 0 {
break
}
}
}

func (g *fortunaGenerator) isCountZero() bool {
for i := len(g.counter) - 1; i >= 0; i-- {
if g.counter[i] != 0 {
return false
}
}

return true
}

func (g *fortunaGenerator) Reseed(seed []byte) {
hash := sha256.New()
hash.Write(g.key)
hash.Write(seed)
g.key = hash.Sum(nil)

aes, err := aes.NewCipher(g.key)
if err != nil {
panic(err)
}

g.cipher = aes
if g.buffer == nil {
g.buffer = bytes.NewBuffer(nil)
}
g.increment()
}

func (g *fortunaGenerator) generateBlocks(blockCount int) ([]byte, error) {
if g.isCountZero() {
return nil, errors.New("PRNG has not been seeded")
}

r := make([]byte, blockCount*16)

for i := 0; i < blockCount; i++ {
g.cipher.Encrypt(r[i*16:], g.counter[:])
g.increment()
}

return r, nil
}

func (g *fortunaGenerator) Read(p []byte) (n int, err error) {
// to be reproducible we will generate data in blocks and buffer them
// so, for example, Read(24) == Read(5) + Read(9)

for len(p) > g.buffer.Len() {
blocks, err := g.generateBlocks(256 / 16)
if err != nil {
return 0, err
}

g.key, err = g.generateBlocks(2)
if err != nil {
return 0, err
}

g.cipher, err = aes.NewCipher(g.key)
if err != nil {
return 0, err
}

g.buffer.Write(blocks)
}

return g.buffer.Read(p)
}

func passKey(password, realm string) []byte {
return pbkdf2.Key([]byte(password), []byte(realm), 4096, 32, sha256.New)
}

func NewDRNG(password, realm string) io.Reader {
rng := &fortunaGenerator{}
rng.Reseed(passKey(password, realm))

return rng
}

func NewDRNGwithSeed(password, realm string, seed []byte) (io.Reader, error) {
uSeed, err := unwrapSeed(password, seed)
if err != nil {
return nil, err
}

// will reuse some of the public seed info
salt := make([]byte, 12+16)
copy(salt[:12], uSeed[:12])
copy(salt[12:], uSeed[len(uSeed)-16:])

hkdf := hkdf.New(sha256.New, uSeed, salt, []byte(realm))
rngSeed := make([]byte, 32)
_, err = io.ReadFull(hkdf, rngSeed)
if err != nil {
return nil, err
}

rng := &fortunaGenerator{}
rng.Reseed(rngSeed)

return rng, nil
}

func GenerateEncryptedKeySeed(password string) ([]byte, error) {
seed := make([]byte, keySeedLength)

_, err := rand.Read(seed)
if err != nil {
return nil, err
}

masterkey := passKey(password, string(seed[:12]))

aes, err := aes.NewCipher(masterkey)
if err != nil {
return nil, err
}

gcm, err := cipher.NewGCM(aes)
if err != nil {
return nil, err
}

pt := seed[12 : len(seed)-16]

// encrypt in place
gcm.Seal(pt[:0], seed[:12], pt, nil)

return seed, nil
}

func unwrapSeed(password string, seed []byte) ([]byte, error) {
masterkey := passKey(password, string(seed[:12]))

aes, err := aes.NewCipher(masterkey)
if err != nil {
return nil, err
}

gcm, err := cipher.NewGCM(aes)
if err != nil {
return nil, err
}

pt := make([]byte, len(seed))
_, err = gcm.Open(pt[12:], seed[:12], seed[12:], nil)
if err != nil {
return nil, err
}

copy(pt[:12], seed[:12])
copy(pt[len(pt)-16:], seed[len(seed)-16:])
return pt, nil
}
Loading

0 comments on commit 6ad4b61

Please sign in to comment.