Compare commits
	
		
			No commits in common. "557f9085f68ca0ecdd696bdd9cc3484b2727f490" and "a8f1a9966762bab2b838940a396c0bd9c3083c22" have entirely different histories.
		
	
	
		
			557f9085f6
			...
			a8f1a99667
		
	
		
							
								
								
									
										131
									
								
								kvdb/kvdb.go
									
									
									
									
									
								
							
							
						
						
									
										131
									
								
								kvdb/kvdb.go
									
									
									
									
									
								
							| @ -1,131 +0,0 @@ | ||||
| package kvdb | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| ) | ||||
| 
 | ||||
| type KVDB struct { | ||||
| 	Prefix string | ||||
| 	Ext    string | ||||
| } | ||||
| 
 | ||||
| func (kv *KVDB) Load( | ||||
| 	keyif interface{}, | ||||
| 	typ ...interface{}, | ||||
| ) (value interface{}, ok bool, err error) { | ||||
| 	key, _ := keyif.(string) | ||||
| 	if "" == key || strings.Contains(key, "..") || strings.ContainsAny(key, "$#!:| \n") { | ||||
| 		return nil, false, nil | ||||
| 	} | ||||
| 
 | ||||
| 	userFile := filepath.Join(kv.Prefix, key+"."+kv.Ext) | ||||
| 	fmt.Println("Debug user file:", userFile) | ||||
| 	b, err := ioutil.ReadFile(userFile) | ||||
| 	if nil != err { | ||||
| 		if os.IsNotExist(err) { | ||||
| 			return nil, false, nil | ||||
| 		} | ||||
| 		fmt.Println("kvdb debug read:", err) | ||||
| 		return nil, false, errors.New("database read failed") | ||||
| 	} | ||||
| 
 | ||||
| 	ok = true | ||||
| 	value = b | ||||
| 	if 1 == len(typ) { | ||||
| 		err := json.Unmarshal(b, typ[0]) | ||||
| 		if nil != err { | ||||
| 			return nil, false, err | ||||
| 		} | ||||
| 		value = typ[0] | ||||
| 	} else if len(b) > 0 && '"' == b[0] { | ||||
| 		var str string | ||||
| 		err := json.Unmarshal(b, &str) | ||||
| 		if nil == err { | ||||
| 			value = str | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return value, ok, nil | ||||
| } | ||||
| 
 | ||||
| func (kv *KVDB) Store(keyif interface{}, value interface{}) (err error) { | ||||
| 	key, _ := keyif.(string) | ||||
| 	if "" == key || strings.Contains(key, "..") || strings.ContainsAny(key, "$#! \n") { | ||||
| 		return errors.New("invalid key name") | ||||
| 	} | ||||
| 
 | ||||
| 	keypath := filepath.Join(kv.Prefix, key+"."+kv.Ext) | ||||
| 	f, err := os.Open(keypath) | ||||
| 	if nil == err { | ||||
| 		s, err := f.Stat() | ||||
| 		if nil != err { | ||||
| 			// if we can open, we should be able to stat | ||||
| 			return errors.New("database connection failure") | ||||
| 		} | ||||
| 		ts := strconv.FormatInt(s.ModTime().Unix(), 10) | ||||
| 		bakpath := filepath.Join(kv.Prefix, key+"."+ts+"."+kv.Ext) | ||||
| 		if err := os.Rename(keypath, bakpath); nil != err { | ||||
| 			// keep the old record as a backup | ||||
| 			return errors.New("database write failure") | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	var b []byte | ||||
| 	switch v := value.(type) { | ||||
| 	case []byte: | ||||
| 		b = v | ||||
| 	case string: | ||||
| 		b, _ = json.Marshal(v) | ||||
| 	default: | ||||
| 		fmt.Println("kvdb: not []byte or string:", v) | ||||
| 		jsonb, err := json.Marshal(v) | ||||
| 		if nil != err { | ||||
| 			return err | ||||
| 		} | ||||
| 		b = jsonb | ||||
| 	} | ||||
| 
 | ||||
| 	if err := ioutil.WriteFile( | ||||
| 		keypath, | ||||
| 		b, | ||||
| 		os.FileMode(0600), | ||||
| 	); nil != err { | ||||
| 		fmt.Println("write failure:", err) | ||||
| 		return errors.New("database write failed") | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (kv *KVDB) Delete(keyif interface{}) (err error) { | ||||
| 	key, _ := keyif.(string) | ||||
| 	if "" == key || strings.Contains(key, "..") || strings.ContainsAny(key, "$#! \n") { | ||||
| 		return errors.New("invalid key name") | ||||
| 	} | ||||
| 
 | ||||
| 	keypath := filepath.Join(kv.Prefix, key+"."+kv.Ext) | ||||
| 	f, err := os.Open(keypath) | ||||
| 	if nil == err { | ||||
| 		s, err := f.Stat() | ||||
| 		if nil != err { | ||||
| 			return errors.New("database connection failure") | ||||
| 		} | ||||
| 		ts := strconv.FormatInt(s.ModTime().Unix(), 64) | ||||
| 		if err := os.Rename(keypath, filepath.Join(kv.Prefix, key+"."+ts+"."+kv.Ext)); nil != err { | ||||
| 			return errors.New("database connection failure") | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (kv *KVDB) Vacuum() (err error) { | ||||
| 	return nil | ||||
| } | ||||
| @ -1,63 +0,0 @@ | ||||
| package kvdb | ||||
| 
 | ||||
| import ( | ||||
| 	"strings" | ||||
| 	"testing" | ||||
| ) | ||||
| 
 | ||||
| type TestEntry struct { | ||||
| 	Email    string   `json:"email"` | ||||
| 	Subjects []string `json:"subjects"` | ||||
| } | ||||
| 
 | ||||
| var email = "john@example.com" | ||||
| var sub = "id123" | ||||
| var dbPrefix = "../testdb" | ||||
| var testKV = &KVDB{ | ||||
| 	Prefix: dbPrefix + "/test-entries", | ||||
| 	Ext:    "eml.json", | ||||
| } | ||||
| 
 | ||||
| func TestStore(t *testing.T) { | ||||
| 	entry := &TestEntry{ | ||||
| 		Email:    email, | ||||
| 		Subjects: []string{sub}, | ||||
| 	} | ||||
| 
 | ||||
| 	if err := testKV.Store(email, entry); nil != err { | ||||
| 		t.Fatal(err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	value, ok, err := testKV.Load(email, &(TestEntry{})) | ||||
| 	if nil != err { | ||||
| 		t.Fatal(err) | ||||
| 		return | ||||
| 	} | ||||
| 	if !ok { | ||||
| 		t.Fatal("test entry not found") | ||||
| 	} | ||||
| 
 | ||||
| 	v, ok := value.(*TestEntry) | ||||
| 	if !ok { | ||||
| 		t.Fatal("test entry not of type TestEntry") | ||||
| 	} | ||||
| 
 | ||||
| 	if email != v.Email || sub != strings.Join(v.Subjects, ",") { | ||||
| 		t.Fatalf("value: %#v", v) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestNoExist(t *testing.T) { | ||||
| 	value, ok, err := testKV.Load("not"+email, &(TestEntry{})) | ||||
| 	if nil != err { | ||||
| 		t.Fatal(err) | ||||
| 		return | ||||
| 	} | ||||
| 	if ok { | ||||
| 		t.Fatal("found entry that doesn't exist") | ||||
| 	} | ||||
| 	if value != nil { | ||||
| 		t.Fatal("had value for entry that doesn't exist") | ||||
| 	} | ||||
| } | ||||
| @ -11,17 +11,35 @@ import ( | ||||
| 	"math/rand" | ||||
| 	mathrand "math/rand" | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"git.coolaj86.com/coolaj86/go-mockid/xkeypairs" | ||||
| ) | ||||
| 
 | ||||
| type Object = map[string]interface{} | ||||
| 
 | ||||
| // options are the things that we may need to know about a request to fulfill it properly | ||||
| type options struct { | ||||
| 	Key     string `json:"key"` | ||||
| 	KeyType string `json:"kty"` | ||||
| 	Seed    int64  `json:"-"` | ||||
| 	SeedStr string `json:"seed"` | ||||
| 	Claims  Object `json:"claims"` | ||||
| 	Header  Object `json:"header"` | ||||
| } | ||||
| 
 | ||||
| // this shananigans is only for testing and debug API stuff | ||||
| func (o *options) nextReader() io.Reader { | ||||
| 	if 0 == o.Seed { | ||||
| 		return RandomReader | ||||
| 	} | ||||
| 	return rand.New(rand.NewSource(o.Seed)) | ||||
| } | ||||
| 
 | ||||
| /* | ||||
| func getJWS(r *http.Request) (*xkeypairs.KeyOptions, error) { | ||||
| func getJWS(r *http.Request) (*options, error) { | ||||
| 
 | ||||
| } | ||||
| */ | ||||
| 
 | ||||
| func getOpts(r *http.Request) (*xkeypairs.KeyOptions, error) { | ||||
| func getOpts(r *http.Request) (*options, error) { | ||||
| 	tok := make(map[string]interface{}) | ||||
| 	decoder := json.NewDecoder(r.Body) | ||||
| 	err := decoder.Decode(&tok) | ||||
| @ -42,17 +60,17 @@ func getOpts(r *http.Request) (*xkeypairs.KeyOptions, error) { | ||||
| 	} | ||||
| 
 | ||||
| 	key, _ := tok["key"].(string) | ||||
| 	opts := &xkeypairs.KeyOptions{ | ||||
| 	opts := &options{ | ||||
| 		Seed: seed, | ||||
| 		Key:  key, | ||||
| 	} | ||||
| 
 | ||||
| 	opts.Claims, _ = tok["claims"].(xkeypairs.Object) | ||||
| 	opts.Header, _ = tok["header"].(xkeypairs.Object) | ||||
| 	opts.Claims, _ = tok["claims"].(Object) | ||||
| 	opts.Header, _ = tok["header"].(Object) | ||||
| 
 | ||||
| 	var n int | ||||
| 	if 0 != seed { | ||||
| 		n = opts.MyFooNextReader().(*mathrand.Rand).Intn(2) | ||||
| 		n = opts.nextReader().(*mathrand.Rand).Intn(2) | ||||
| 	} else { | ||||
| 		n = rand.Intn(2) | ||||
| 	} | ||||
|  | ||||
| @ -1,12 +1,21 @@ | ||||
| package api | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto/ecdsa" | ||||
| 	"crypto/elliptic" | ||||
| 	"crypto/rand" | ||||
| 	"crypto/rsa" | ||||
| 	"io" | ||||
| 	"log" | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"git.coolaj86.com/coolaj86/go-mockid/xkeypairs" | ||||
| 	"git.rootprojects.org/root/keypairs" | ||||
| ) | ||||
| 
 | ||||
| // RandomReader may be overwritten for testing | ||||
| var RandomReader io.Reader = rand.Reader | ||||
| 
 | ||||
| // GeneratePublicJWK will create a new private key in JWK format | ||||
| func GeneratePublicJWK(w http.ResponseWriter, r *http.Request) { | ||||
| 	if "POST" != r.Method { | ||||
| @ -43,7 +52,7 @@ func GeneratePrivateJWK(w http.ResponseWriter, r *http.Request) { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	privkey := xkeypairs.GenPrivKey(opts) | ||||
| 	privkey := genPrivKey(opts) | ||||
| 
 | ||||
| 	jwk := xkeypairs.MarshalJWKPrivateKey(privkey) | ||||
| 	w.Write(append(jwk, '\n')) | ||||
| @ -86,7 +95,7 @@ func GeneratePrivateDER(w http.ResponseWriter, r *http.Request) { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	privkey := xkeypairs.GenPrivKey(opts) | ||||
| 	privkey := genPrivKey(opts) | ||||
| 
 | ||||
| 	der, _ := xkeypairs.MarshalDERPrivateKey(privkey) | ||||
| 	w.Write(der) | ||||
| @ -129,7 +138,7 @@ func GeneratePrivatePEM(w http.ResponseWriter, r *http.Request) { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	privkey := xkeypairs.GenPrivKey(opts) | ||||
| 	privkey := genPrivKey(opts) | ||||
| 
 | ||||
| 	privpem, _ := xkeypairs.MarshalPEMPrivateKey(privkey) | ||||
| 	w.Write(privpem) | ||||
| @ -137,9 +146,39 @@ func GeneratePrivatePEM(w http.ResponseWriter, r *http.Request) { | ||||
| 
 | ||||
| const maxRetry = 16 | ||||
| 
 | ||||
| func getPrivKey(opts *xkeypairs.KeyOptions) (keypairs.PrivateKey, error) { | ||||
| func getPrivKey(opts *options) (keypairs.PrivateKey, error) { | ||||
| 	if "" != opts.Key { | ||||
| 		return keypairs.ParsePrivateKey([]byte(opts.Key)) | ||||
| 	} | ||||
| 	return xkeypairs.GenPrivKey(opts), nil | ||||
| 	return genPrivKey(opts), nil | ||||
| } | ||||
| 
 | ||||
| func genPrivKey(opts *options) keypairs.PrivateKey { | ||||
| 	var privkey keypairs.PrivateKey | ||||
| 
 | ||||
| 	if "RSA" == opts.KeyType { | ||||
| 		keylen := 2048 | ||||
| 		privkey, _ = rsa.GenerateKey(opts.nextReader(), keylen) | ||||
| 		if 0 != opts.Seed { | ||||
| 			for i := 0; i < maxRetry; i++ { | ||||
| 				otherkey, _ := rsa.GenerateKey(opts.nextReader(), keylen) | ||||
| 				otherCmp := otherkey.D.Cmp(privkey.(*rsa.PrivateKey).D) | ||||
| 				if 0 != otherCmp { | ||||
| 					// There are two possible keys, choose the lesser D value | ||||
| 					// See https://github.com/square/go-jose/issues/189 | ||||
| 					if otherCmp < 0 { | ||||
| 						privkey = otherkey | ||||
| 					} | ||||
| 					break | ||||
| 				} | ||||
| 				if maxRetry == i-1 { | ||||
| 					log.Printf("error: coinflip landed on heads %d times", maxRetry) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} else { | ||||
| 		// TODO: EC keys may also suffer the same random problems in the future | ||||
| 		privkey, _ = ecdsa.GenerateKey(elliptic.P256(), opts.nextReader()) | ||||
| 	} | ||||
| 	return privkey | ||||
| } | ||||
|  | ||||
| @ -1,61 +0,0 @@ | ||||
| package xkeypairs | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto/ecdsa" | ||||
| 	"crypto/elliptic" | ||||
| 	"crypto/rsa" | ||||
| 	"io" | ||||
| 	"log" | ||||
| 	"math/rand" | ||||
| 
 | ||||
| 	"git.rootprojects.org/root/keypairs" | ||||
| ) | ||||
| 
 | ||||
| // KeyOptions are the things that we may need to know about a request to fulfill it properly | ||||
| type KeyOptions struct { | ||||
| 	Key     string `json:"key"` | ||||
| 	KeyType string `json:"kty"` | ||||
| 	Seed    int64  `json:"-"` | ||||
| 	SeedStr string `json:"seed"` | ||||
| 	Claims  Object `json:"claims"` | ||||
| 	Header  Object `json:"header"` | ||||
| } | ||||
| 
 | ||||
| // this shananigans is only for testing and debug API stuff | ||||
| func (o *KeyOptions) MyFooNextReader() io.Reader { | ||||
| 	if 0 == o.Seed { | ||||
| 		return RandomReader | ||||
| 	} | ||||
| 	return rand.New(rand.NewSource(o.Seed)) | ||||
| } | ||||
| 
 | ||||
| // GenPrivKey generates a 256-bit entropy RSA or ECDSA private key | ||||
| func GenPrivKey(opts *KeyOptions) keypairs.PrivateKey { | ||||
| 	var privkey keypairs.PrivateKey | ||||
| 
 | ||||
| 	if "RSA" == opts.KeyType { | ||||
| 		keylen := 2048 | ||||
| 		privkey, _ = rsa.GenerateKey(opts.MyFooNextReader(), keylen) | ||||
| 		if 0 != opts.Seed { | ||||
| 			for i := 0; i < maxRetry; i++ { | ||||
| 				otherkey, _ := rsa.GenerateKey(opts.MyFooNextReader(), keylen) | ||||
| 				otherCmp := otherkey.D.Cmp(privkey.(*rsa.PrivateKey).D) | ||||
| 				if 0 != otherCmp { | ||||
| 					// There are two possible keys, choose the lesser D value | ||||
| 					// See https://github.com/square/go-jose/issues/189 | ||||
| 					if otherCmp < 0 { | ||||
| 						privkey = otherkey | ||||
| 					} | ||||
| 					break | ||||
| 				} | ||||
| 				if maxRetry == i-1 { | ||||
| 					log.Printf("error: coinflip landed on heads %d times", maxRetry) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} else { | ||||
| 		// TODO: EC keys may also suffer the same random problems in the future | ||||
| 		privkey, _ = ecdsa.GenerateKey(elliptic.P256(), opts.MyFooNextReader()) | ||||
| 	} | ||||
| 	return privkey | ||||
| } | ||||
| @ -1,7 +1,7 @@ | ||||
| package xkeypairs | ||||
| 
 | ||||
| import ( | ||||
| 	"io/ioutil" | ||||
| 	"strconv" | ||||
| 
 | ||||
| 	"git.rootprojects.org/root/keypairs" | ||||
| ) | ||||
| @ -12,11 +12,37 @@ func ParsePEMPrivateKey(block []byte) (keypairs.PrivateKey, error) { | ||||
| 	return keypairs.ParsePrivateKey(block) | ||||
| } | ||||
| 
 | ||||
| // ParsePrivateKeyFile returns the private key from the given file path, if available | ||||
| func ParsePrivateKeyFile(pathname string) (keypairs.PrivateKey, error) { | ||||
| 	block, err := ioutil.ReadFile(pathname) | ||||
| func ParseDuration(exp string) (int, error) { | ||||
| 	if "" == exp { | ||||
| 		exp = "15m" | ||||
| 	} | ||||
| 
 | ||||
| 	mult := 1 | ||||
| 	switch exp[len(exp)-1] { | ||||
| 	case 'w': | ||||
| 		mult *= 7 | ||||
| 		fallthrough | ||||
| 	case 'd': | ||||
| 		mult *= 24 | ||||
| 		fallthrough | ||||
| 	case 'h': | ||||
| 		mult *= 60 | ||||
| 		fallthrough | ||||
| 	case 'm': | ||||
| 		mult *= 60 | ||||
| 		fallthrough | ||||
| 	case 's': | ||||
| 		// no fallthrough | ||||
| 	default: | ||||
| 		// could be 'k' or 'z', but we assume its empty | ||||
| 		exp += "s" | ||||
| 	} | ||||
| 
 | ||||
| 	// 15m => num=15, mult=1*60 | ||||
| 	num, err := strconv.Atoi(exp[:len(exp)-1]) | ||||
| 	if nil != err { | ||||
| 		return nil, err | ||||
| 		return 0, err | ||||
| 	} | ||||
| 	return keypairs.ParsePrivateKey(block) | ||||
| 
 | ||||
| 	return num * mult, nil | ||||
| } | ||||
|  | ||||
| @ -17,10 +17,7 @@ import ( | ||||
| 	"git.rootprojects.org/root/keypairs" | ||||
| ) | ||||
| 
 | ||||
| // RandomReader may be overwritten for testing | ||||
| var RandomReader io.Reader = rand.Reader | ||||
| 
 | ||||
| //var RandomReader = rand.Reader | ||||
| var RandomReader = rand.Reader | ||||
| 
 | ||||
| type JWS struct { | ||||
| 	Header    Object `json:"header"`    // JSON | ||||
| @ -32,7 +29,6 @@ type JWS struct { | ||||
| 
 | ||||
| type Object = map[string]interface{} | ||||
| 
 | ||||
| // SignClaims adds `typ`, `kid` (or `jwk`), and `alg` in the header and expects claims for `jti`, `exp`, `iss`, and `iat` | ||||
| func SignClaims(privkey keypairs.PrivateKey, header Object, claims Object) (*JWS, error) { | ||||
| 	var randsrc io.Reader = RandomReader | ||||
| 	seed, _ := header["_seed"].(int64) | ||||
| @ -123,12 +119,11 @@ func claimsToPayload(claims Object) ([]byte, error) { | ||||
| 
 | ||||
| 	// parse if exp is actually a duration, such as "15m" | ||||
| 	if 0 == exp && "" != dur { | ||||
| 		s, err := time.ParseDuration(dur) | ||||
| 		// TODO s, err := time.ParseDuration(dur) | ||||
| 		s, err := ParseDuration(dur) | ||||
| 		if nil != err { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		exp = time.Now().Add(s * time.Second).Unix() | ||||
| 		exp = time.Now().Add(time.Duration(s) * time.Second).Unix() | ||||
| 		claims["exp"] = exp | ||||
| 	} | ||||
| 	if "" == jti && 0 == exp && !insecure { | ||||
| @ -160,6 +155,8 @@ func Sign(rand io.Reader, privkey keypairs.PrivateKey, hash []byte) []byte { | ||||
| 	case *ecdsa.PrivateKey: | ||||
| 		r, s, _ := ecdsa.Sign(rand, k, hash[:]) | ||||
| 		rb := r.Bytes() | ||||
| 		fmt.Println("debug:") | ||||
| 		fmt.Println(r, s) | ||||
| 		for len(rb) < 32 { | ||||
| 			rb = append([]byte{0}, rb...) | ||||
| 		} | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user