mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-31 02:28:09 +00:00 
			
		
		
		
	Provide base64 keys in addition to hex encoded. (#1734)
* Provide base64 keys in addition to hex encoded. Accept these at unseal/rekey time. Also fix a bug where backup would not be honored when doing a rekey with no operation currently ongoing.
This commit is contained in:
		| @@ -46,6 +46,8 @@ type InitStatusResponse struct { | ||||
|  | ||||
| type InitResponse struct { | ||||
| 	Keys            []string `json:"keys"` | ||||
| 	KeysB64         []string `json:"keys_base64"` | ||||
| 	RecoveryKeys    []string `json:"recovery_keys"` | ||||
| 	RecoveryKeysB64 []string `json:"recovery_keys_base64"` | ||||
| 	RootToken       string   `json:"root_token"` | ||||
| } | ||||
|   | ||||
| @@ -190,6 +190,7 @@ type RekeyUpdateResponse struct { | ||||
| 	Nonce           string | ||||
| 	Complete        bool | ||||
| 	Keys            []string | ||||
| 	KeysB64         []string `json:"keys_base64"` | ||||
| 	PGPFingerprints []string `json:"pgp_fingerprints"` | ||||
| 	Backup          bool | ||||
| } | ||||
| @@ -197,4 +198,5 @@ type RekeyUpdateResponse struct { | ||||
| type RekeyRetrieveResponse struct { | ||||
| 	Nonce   string | ||||
| 	Keys    map[string][]string | ||||
| 	KeysB64 map[string][]string `json:"keys_base64"` | ||||
| } | ||||
|   | ||||
| @@ -192,11 +192,21 @@ func (c *InitCommand) runInit(check bool, initRequest *api.InitRequest) int { | ||||
| 	} | ||||
|  | ||||
| 	for i, key := range resp.Keys { | ||||
| 		if resp.KeysB64 != nil && len(resp.KeysB64) == len(resp.Keys) { | ||||
| 			c.Ui.Output(fmt.Sprintf("Unseal Key %d (hex)   : %s", i+1, key)) | ||||
| 			c.Ui.Output(fmt.Sprintf("Unseal Key %d (base64): %s", i+1, resp.KeysB64[i])) | ||||
| 		} else { | ||||
| 			c.Ui.Output(fmt.Sprintf("Unseal Key %d: %s", i+1, key)) | ||||
| 		} | ||||
| 	} | ||||
| 	for i, key := range resp.RecoveryKeys { | ||||
| 		if resp.RecoveryKeysB64 != nil && len(resp.RecoveryKeysB64) == len(resp.RecoveryKeys) { | ||||
| 			c.Ui.Output(fmt.Sprintf("Recovery Key %d (hex)   : %s", i+1, key)) | ||||
| 			c.Ui.Output(fmt.Sprintf("Recovery Key %d (base64): %s", i+1, resp.RecoveryKeysB64[i])) | ||||
| 		} else { | ||||
| 			c.Ui.Output(fmt.Sprintf("Recovery Key %d: %s", i+1, key)) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	c.Ui.Output(fmt.Sprintf("Initial Root Token: %s", resp.RootToken)) | ||||
|  | ||||
|   | ||||
| @@ -244,5 +244,5 @@ func TestInit_PGP(t *testing.T) { | ||||
|  | ||||
| 	rootToken := matches[0][1] | ||||
|  | ||||
| 	parseDecryptAndTestUnsealKeys(t, ui.OutputWriter.String(), rootToken, false, nil, core) | ||||
| 	parseDecryptAndTestUnsealKeys(t, ui.OutputWriter.String(), rootToken, false, nil, nil, core) | ||||
| } | ||||
|   | ||||
| @@ -66,7 +66,9 @@ func parseDecryptAndTestUnsealKeys(t *testing.T, | ||||
| 	input, rootToken string, | ||||
| 	fingerprints bool, | ||||
| 	backupKeys map[string][]string, | ||||
| 	backupKeysB64 map[string][]string, | ||||
| 	core *vault.Core) { | ||||
|  | ||||
| 	decoder := base64.StdEncoding | ||||
| 	priv1Bytes, err := decoder.DecodeString(pgpkeys.TestPrivKey1) | ||||
| 	if err != nil { | ||||
| @@ -87,11 +89,20 @@ func parseDecryptAndTestUnsealKeys(t *testing.T, | ||||
| 		priv3Bytes, | ||||
| 	} | ||||
|  | ||||
| 	testFunc := func(b64 bool, bkeys map[string][]string) { | ||||
| 		var re *regexp.Regexp | ||||
| 		if fingerprints { | ||||
| 		re, err = regexp.Compile("\\s*Key\\s+\\d+\\s+fingerprint:\\s+([0-9a-fA-F]+);\\s+value:\\s+(.*)") | ||||
| 			if b64 { | ||||
| 				re, err = regexp.Compile("\\s*Key\\s+\\d+\\s+fingerprint:\\s+([0-9a-fA-F]+);\\s+value\\s+\\(base64\\):\\s+(.*)") | ||||
| 			} else { | ||||
| 		re, err = regexp.Compile("\\s*Key\\s+\\d+:\\s+(.*)") | ||||
| 				re, err = regexp.Compile("\\s*Key\\s+\\d+\\s+fingerprint:\\s+([0-9a-fA-F]+);\\s+value\\s+\\(hex\\)\\s+:\\s+(.*)") | ||||
| 			} | ||||
| 		} else { | ||||
| 			if b64 { | ||||
| 				re, err = regexp.Compile("\\s*Key\\s+\\d+\\s\\(base64\\):\\s+(.*)") | ||||
| 			} else { | ||||
| 				re, err = regexp.Compile("\\s*Key\\s+\\d+\\s\\(hex\\)\\s+:\\s+(.*)") | ||||
| 			} | ||||
| 		} | ||||
| 		if err != nil { | ||||
| 			t.Fatalf("Error compiling regex: %s", err) | ||||
| @@ -118,14 +129,14 @@ func parseDecryptAndTestUnsealKeys(t *testing.T, | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 	if backupKeys != nil && len(matchedFingerprints) != 0 { | ||||
| 		if bkeys != nil && len(matchedFingerprints) != 0 { | ||||
| 			testMap := map[string][]string{} | ||||
| 			for i, v := range matchedFingerprints { | ||||
| 				testMap[v] = append(testMap[v], encodedKeys[i]) | ||||
| 				sort.Strings(testMap[v]) | ||||
| 			} | ||||
| 		if !reflect.DeepEqual(testMap, backupKeys) { | ||||
| 			t.Fatalf("test map and backup map do not match, test map is\n%#v\nbackup map is\n%#v", testMap, backupKeys) | ||||
| 			if !reflect.DeepEqual(testMap, bkeys) { | ||||
| 				t.Fatalf("test map and backup map do not match, test map is\n%#v\nbackup map is\n%#v", testMap, bkeys) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| @@ -140,9 +151,14 @@ func parseDecryptAndTestUnsealKeys(t *testing.T, | ||||
| 			if err != nil { | ||||
| 				t.Fatalf("Error parsing private key %d: %s", i, err) | ||||
| 			} | ||||
| 		keyBytes, err := hex.DecodeString(encodedKeys[i]) | ||||
| 			var keyBytes []byte | ||||
| 			if b64 { | ||||
| 				keyBytes, err = base64.StdEncoding.DecodeString(encodedKeys[i]) | ||||
| 			} else { | ||||
| 				keyBytes, err = hex.DecodeString(encodedKeys[i]) | ||||
| 			} | ||||
| 			if err != nil { | ||||
| 			t.Fatalf("Error hex-decoding key %d: %s", i, err) | ||||
| 				t.Fatalf("Error decoding key %d: %s", i, err) | ||||
| 			} | ||||
| 			entityList := &openpgp.EntityList{entity} | ||||
| 			md, err := openpgp.ReadMessage(bytes.NewBuffer(keyBytes), entityList, nil, nil) | ||||
| @@ -171,5 +187,8 @@ func parseDecryptAndTestUnsealKeys(t *testing.T, | ||||
| 				t.Fatalf("Error: Provided two unseal keys but core is not unsealed") | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	testFunc(false, backupKeys) | ||||
| 	testFunc(true, backupKeysB64) | ||||
| } | ||||
|   | ||||
| @@ -93,12 +93,14 @@ func (c *RekeyCommand) Run(args []string) int { | ||||
| 				SecretShares:    shares, | ||||
| 				SecretThreshold: threshold, | ||||
| 				PGPKeys:         pgpKeys, | ||||
| 				Backup:          backup, | ||||
| 			}) | ||||
| 		} else { | ||||
| 			rekeyStatus, err = client.Sys().RekeyInit(&api.RekeyInitRequest{ | ||||
| 				SecretShares:    shares, | ||||
| 				SecretThreshold: threshold, | ||||
| 				PGPKeys:         pgpKeys, | ||||
| 				Backup:          backup, | ||||
| 			}) | ||||
| 		} | ||||
| 		if err != nil { | ||||
| @@ -158,13 +160,27 @@ func (c *RekeyCommand) Run(args []string) int { | ||||
| 	// Space between the key prompt, if any, and the output | ||||
| 	c.Ui.Output("\n") | ||||
| 	// Provide the keys | ||||
| 	var haveB64 bool | ||||
| 	if result.KeysB64 != nil && len(result.KeysB64) == len(result.Keys) { | ||||
| 		haveB64 = true | ||||
| 	} | ||||
| 	for i, key := range result.Keys { | ||||
| 		if len(result.PGPFingerprints) > 0 { | ||||
| 			if haveB64 { | ||||
| 				c.Ui.Output(fmt.Sprintf("Key %d fingerprint: %s; value (hex)   : %s", i+1, result.PGPFingerprints[i], key)) | ||||
| 				c.Ui.Output(fmt.Sprintf("Key %d fingerprint: %s; value (base64): %s", i+1, result.PGPFingerprints[i], result.KeysB64[i])) | ||||
| 			} else { | ||||
| 				c.Ui.Output(fmt.Sprintf("Key %d fingerprint: %s; value: %s", i+1, result.PGPFingerprints[i], key)) | ||||
| 			} | ||||
| 		} else { | ||||
| 			if haveB64 { | ||||
| 				c.Ui.Output(fmt.Sprintf("Key %d (hex)   : %s", i+1, key)) | ||||
| 				c.Ui.Output(fmt.Sprintf("Key %d (base64): %s", i+1, result.KeysB64[i])) | ||||
| 			} else { | ||||
| 				c.Ui.Output(fmt.Sprintf("Key %d: %s", i+1, key)) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	c.Ui.Output(fmt.Sprintf("\nOperation nonce: %s", result.Nonce)) | ||||
|  | ||||
|   | ||||
| @@ -228,6 +228,7 @@ func TestRekey_init_pgp(t *testing.T) { | ||||
|  | ||||
| 	type backupStruct struct { | ||||
| 		Keys    map[string][]string | ||||
| 		KeysB64 map[string][]string | ||||
| 	} | ||||
| 	backupVals := &backupStruct{} | ||||
|  | ||||
| @@ -247,6 +248,7 @@ func TestRekey_init_pgp(t *testing.T) { | ||||
| 	} | ||||
|  | ||||
| 	backupVals.Keys = resp.Data["keys"].(map[string][]string) | ||||
| 	backupVals.KeysB64 = resp.Data["keys_base64"].(map[string][]string) | ||||
|  | ||||
| 	// Now delete and try again; the values should be inaccessible | ||||
| 	req = logical.TestRequest(t, logical.DeleteOperation, "rekey/backup") | ||||
| @@ -269,7 +271,8 @@ func TestRekey_init_pgp(t *testing.T) { | ||||
| 	// Sort, because it'll be tested with DeepEqual later | ||||
| 	for k, _ := range backupVals.Keys { | ||||
| 		sort.Strings(backupVals.Keys[k]) | ||||
| 		sort.Strings(backupVals.KeysB64[k]) | ||||
| 	} | ||||
|  | ||||
| 	parseDecryptAndTestUnsealKeys(t, ui.OutputWriter.String(), token, true, backupVals.Keys, core) | ||||
| 	parseDecryptAndTestUnsealKeys(t, ui.OutputWriter.String(), token, true, backupVals.Keys, backupVals.KeysB64, core) | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| package command | ||||
|  | ||||
| import ( | ||||
| 	"encoding/base64" | ||||
| 	"encoding/hex" | ||||
| 	"fmt" | ||||
| 	"log" | ||||
| @@ -489,8 +490,9 @@ func (c *ServerCommand) Run(args []string) int { | ||||
| 				"    "+export+" VAULT_ADDR="+quote+"http://"+config.Listeners[0].Config["address"]+quote+"\n\n"+ | ||||
| 				"The unseal key and root token are reproduced below in case you\n"+ | ||||
| 				"want to seal/unseal the Vault or play with authentication.\n\n"+ | ||||
| 				"Unseal Key: %s\nRoot Token: %s\n", | ||||
| 				"Unseal Key (hex)   : %s\nUnseal Key (base64): %s\nRoot Token: %s\n", | ||||
| 			hex.EncodeToString(init.SecretShares[0]), | ||||
| 			base64.StdEncoding.EncodeToString(init.SecretShares[0]), | ||||
| 			init.RootToken, | ||||
| 		)) | ||||
| 	} | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| package http | ||||
|  | ||||
| import ( | ||||
| 	"encoding/base64" | ||||
| 	"encoding/hex" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| @@ -123,14 +124,21 @@ func handleSysGenerateRootUpdate(core *vault.Core) http.Handler { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		// Decode the key, which is hex encoded | ||||
| 		// Decode the key, which is base64 or hex encoded | ||||
| 		min, max := core.BarrierKeyLength() | ||||
| 		key, err := hex.DecodeString(req.Key) | ||||
| 		// We check min and max here to ensure that a string that is base64 | ||||
| 		// encoded but also valid hex will not be valid and we instead base64 | ||||
| 		// decode it | ||||
| 		if err != nil || len(key) < min || len(key) > max { | ||||
| 			key, err = base64.StdEncoding.DecodeString(req.Key) | ||||
| 			if err != nil { | ||||
| 				respondError( | ||||
| 					w, http.StatusBadRequest, | ||||
| 				errors.New("'key' must be a valid hex-string")) | ||||
| 					errors.New("'key' must be a valid hex or base64 string")) | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// Use the key to make progress on root generation | ||||
| 		result, err := core.GenerateRootUpdate(key, req.Nonce) | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| package http | ||||
|  | ||||
| import ( | ||||
| 	"encoding/base64" | ||||
| 	"encoding/hex" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| @@ -93,19 +94,24 @@ func handleSysInitPut(core *vault.Core, w http.ResponseWriter, r *http.Request) | ||||
|  | ||||
| 	// Encode the keys | ||||
| 	keys := make([]string, 0, len(result.SecretShares)) | ||||
| 	keysB64 := make([]string, 0, len(result.SecretShares)) | ||||
| 	for _, k := range result.SecretShares { | ||||
| 		keys = append(keys, hex.EncodeToString(k)) | ||||
| 		keysB64 = append(keysB64, base64.StdEncoding.EncodeToString(k)) | ||||
| 	} | ||||
|  | ||||
| 	resp := &InitResponse{ | ||||
| 		Keys:      keys, | ||||
| 		KeysB64:   keysB64, | ||||
| 		RootToken: result.RootToken, | ||||
| 	} | ||||
|  | ||||
| 	if len(result.RecoveryShares) > 0 { | ||||
| 		resp.RecoveryKeys = make([]string, 0, len(result.RecoveryShares)) | ||||
| 		resp.RecoveryKeysB64 = make([]string, 0, len(result.RecoveryShares)) | ||||
| 		for _, k := range result.RecoveryShares { | ||||
| 			resp.RecoveryKeys = append(resp.RecoveryKeys, hex.EncodeToString(k)) | ||||
| 			resp.RecoveryKeysB64 = append(resp.RecoveryKeysB64, base64.StdEncoding.EncodeToString(k)) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| @@ -126,7 +132,9 @@ type InitRequest struct { | ||||
|  | ||||
| type InitResponse struct { | ||||
| 	Keys            []string `json:"keys"` | ||||
| 	KeysB64         []string `json:"keys_base64"` | ||||
| 	RecoveryKeys    []string `json:"recovery_keys,omitempty"` | ||||
| 	RecoveryKeysB64 []string `json:"recovery_keys_base64,omitempty"` | ||||
| 	RootToken       string   `json:"root_token"` | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| package http | ||||
|  | ||||
| import ( | ||||
| 	"encoding/base64" | ||||
| 	"encoding/hex" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| @@ -146,14 +147,21 @@ func handleSysRekeyUpdate(core *vault.Core, recovery bool) http.Handler { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		// Decode the key, which is hex encoded | ||||
| 		// Decode the key, which is base64 or hex encoded | ||||
| 		min, max := core.BarrierKeyLength() | ||||
| 		key, err := hex.DecodeString(req.Key) | ||||
| 		// We check min and max here to ensure that a string that is base64 | ||||
| 		// encoded but also valid hex will not be valid and we instead base64 | ||||
| 		// decode it | ||||
| 		if err != nil || len(key) < min || len(key) > max { | ||||
| 			key, err = base64.StdEncoding.DecodeString(req.Key) | ||||
| 			if err != nil { | ||||
| 				respondError( | ||||
| 					w, http.StatusBadRequest, | ||||
| 				errors.New("'key' must be a valid hex-string")) | ||||
| 					errors.New("'key' must be a valid hex or base64 string")) | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// Use the key to make progress on rekey | ||||
| 		result, err := core.RekeyUpdate(key, req.Nonce, recovery) | ||||
| @@ -167,16 +175,18 @@ func handleSysRekeyUpdate(core *vault.Core, recovery bool) http.Handler { | ||||
| 		if result != nil { | ||||
| 			resp.Complete = true | ||||
| 			resp.Nonce = req.Nonce | ||||
| 			resp.Backup = result.Backup | ||||
| 			resp.PGPFingerprints = result.PGPFingerprints | ||||
|  | ||||
| 			// Encode the keys | ||||
| 			keys := make([]string, 0, len(result.SecretShares)) | ||||
| 			keysB64 := make([]string, 0, len(result.SecretShares)) | ||||
| 			for _, k := range result.SecretShares { | ||||
| 				keys = append(keys, hex.EncodeToString(k)) | ||||
| 				keysB64 = append(keysB64, base64.StdEncoding.EncodeToString(k)) | ||||
| 			} | ||||
| 			resp.Keys = keys | ||||
|  | ||||
| 			resp.Backup = result.Backup | ||||
| 			resp.PGPFingerprints = result.PGPFingerprints | ||||
| 			resp.KeysB64 = keysB64 | ||||
| 		} | ||||
| 		respondOk(w, resp) | ||||
| 	}) | ||||
| @@ -210,6 +220,7 @@ type RekeyUpdateResponse struct { | ||||
| 	Nonce           string   `json:"nonce"` | ||||
| 	Complete        bool     `json:"complete"` | ||||
| 	Keys            []string `json:"keys"` | ||||
| 	KeysB64         []string `json:"keys_base64"` | ||||
| 	PGPFingerprints []string `json:"pgp_fingerprints"` | ||||
| 	Backup          bool     `json:"backup"` | ||||
| } | ||||
|   | ||||
| @@ -180,8 +180,13 @@ func TestSysRekey_Update(t *testing.T) { | ||||
| 	if len(keys) != 5 { | ||||
| 		t.Fatalf("bad: %#v", keys) | ||||
| 	} | ||||
| 	keysB64 := actual["keys_base64"].([]interface{}) | ||||
| 	if len(keysB64) != 5 { | ||||
| 		t.Fatalf("bad: %#v", keysB64) | ||||
| 	} | ||||
|  | ||||
| 	delete(actual, "keys") | ||||
| 	delete(actual, "keys_base64") | ||||
| 	if !reflect.DeepEqual(actual, expected) { | ||||
| 		t.Fatalf("\nexpected: %#v\nactual: %#v", expected, actual) | ||||
| 	} | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| package http | ||||
|  | ||||
| import ( | ||||
| 	"encoding/base64" | ||||
| 	"encoding/hex" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| @@ -97,14 +98,21 @@ func handleSysUnseal(core *vault.Core) http.Handler { | ||||
| 			} | ||||
| 			core.ResetUnsealProcess() | ||||
| 		} else { | ||||
| 			// Decode the key, which is hex encoded | ||||
| 			// Decode the key, which is base64 or hex encoded | ||||
| 			min, max := core.BarrierKeyLength() | ||||
| 			key, err := hex.DecodeString(req.Key) | ||||
| 			// We check min and max here to ensure that a string that is base64 | ||||
| 			// encoded but also valid hex will not be valid and we instead base64 | ||||
| 			// decode it | ||||
| 			if err != nil || len(key) < min || len(key) > max { | ||||
| 				key, err = base64.StdEncoding.DecodeString(req.Key) | ||||
| 				if err != nil { | ||||
| 					respondError( | ||||
| 						w, http.StatusBadRequest, | ||||
| 					errors.New("'key' must be a valid hex-string")) | ||||
| 						errors.New("'key' must be a valid hex or base64 string")) | ||||
| 					return | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			// Attempt the unseal | ||||
| 			if _, err := core.Unseal(key); err != nil { | ||||
|   | ||||
| @@ -1490,3 +1490,9 @@ func (c *Core) SealAccess() *SealAccess { | ||||
| func (c *Core) Logger() *log.Logger { | ||||
| 	return c.logger | ||||
| } | ||||
|  | ||||
| func (c *Core) BarrierKeyLength() (min, max int) { | ||||
| 	min, max = c.barrier.KeyLength() | ||||
| 	max += shamir.ShareOverhead | ||||
| 	return | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,8 @@ | ||||
| package vault | ||||
|  | ||||
| import ( | ||||
| 	"encoding/base64" | ||||
| 	"encoding/hex" | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| @@ -607,11 +609,28 @@ func (b *SystemBackend) handleRekeyRetrieve( | ||||
| 		return logical.ErrorResponse("no backed-up keys found"), nil | ||||
| 	} | ||||
|  | ||||
| 	keysB64 := map[string][]string{} | ||||
| 	for k, v := range backup.Keys { | ||||
| 		for _, j := range v { | ||||
| 			currB64Keys := keysB64[k] | ||||
| 			if currB64Keys == nil { | ||||
| 				currB64Keys = []string{} | ||||
| 			} | ||||
| 			key, err := hex.DecodeString(j) | ||||
| 			if err != nil { | ||||
| 				return nil, fmt.Errorf("error decoding hex-encoded backup key: %v", err) | ||||
| 			} | ||||
| 			currB64Keys = append(currB64Keys, base64.StdEncoding.EncodeToString(key)) | ||||
| 			keysB64[k] = currB64Keys | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Format the status | ||||
| 	resp := &logical.Response{ | ||||
| 		Data: map[string]interface{}{ | ||||
| 			"nonce":       backup.Nonce, | ||||
| 			"keys":        backup.Keys, | ||||
| 			"keys_base64": keysB64, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Jeff Mitchell
					Jeff Mitchell