From d740d5fd91a64e23e7b6a11aa424ccedead2a513 Mon Sep 17 00:00:00 2001
From: Daniel <dhaavi@users.noreply.github.com>
Date: Fri, 24 Jan 2020 16:06:27 +0100
Subject: [PATCH] Update CLI to reflect change done in audit remediation

---
 .gitignore                        |   2 +
 cmd/{doc.go => README.md}         |  24 +++---
 cmd/cfg-envelope.go               | 124 ++++++++++--------------------
 cmd/cfg-signet.go                 |  52 +------------
 cmd/cfg-tools.go                  |   6 +-
 cmd/cmd-close.go                  |  10 ++-
 cmd/cmd-configure.go              |   9 ++-
 cmd/cmd-generate.go               |   9 ++-
 cmd/{cmd-tools.go => cmd-list.go} |  32 ++++++--
 cmd/cmd-manage.go                 |  15 ++--
 cmd/cmd-open.go                   |   9 ++-
 cmd/cmd-verify.go                 |   5 +-
 cmd/format.go                     |  24 ++++--
 13 files changed, 141 insertions(+), 180 deletions(-)
 rename cmd/{doc.go => README.md} (61%)
 rename cmd/{cmd-tools.go => cmd-list.go} (57%)

diff --git a/.gitignore b/.gitignore
index 1926fb7..dccf69d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,4 @@
 cpu.out
 vendor
+cmd/cmd
+cmd/jess
diff --git a/cmd/doc.go b/cmd/README.md
similarity index 61%
rename from cmd/doc.go
rename to cmd/README.md
index 46a7e4f..1146e1e 100644
--- a/cmd/doc.go
+++ b/cmd/README.md
@@ -1,9 +1,12 @@
-/*
-CLI:
+### Jess CLI
 
+This is currently still more of a planning and working document.  
+Here is the CLI interface that is planned:
+
+```
 jess create <envelope file>
 
-jess close <file> with <envelope file>
+jess close <file> with <envelope name>
 	encrypt a file, write to file with the same name, but with a .letter suffix
 	-o <file> ... write output to <file>
 
@@ -23,11 +26,12 @@ jess show <file>
 	- letter
 	- seal (signature-only letter)
 
-global arguments
---tsdir /path/to/truststore
---seclevel <uint>
---symkeysize <uint>
---quiet only output errors and warnings
+jess generate
+    generate a new signet and store both signet and recipient in the truststore
 
-*/
-package main
+global arguments
+    --tsdir /path/to/truststore
+    --seclevel <uint>
+    --symkeysize <uint>
+    --quiet only output errors and warnings
+```
diff --git a/cmd/cfg-envelope.go b/cmd/cfg-envelope.go
index e16a42a..6819292 100644
--- a/cmd/cfg-envelope.go
+++ b/cmd/cfg-envelope.go
@@ -26,10 +26,10 @@ func newEnvelope(name string) (*jess.Envelope, error) {
 		Message: "Select preset:",
 		Options: []string{
 			"Encrypt with password",
-			"Encrypt with keyfile",
-			"Encrypt for someone",
+			"Encrypt with key",
+			"Encrypt for someone and sign",
+			"Encrypt for someone but don't sign",
 			"Sign a file",
-			"Custom from scratch",
 		},
 	}
 	err := survey.AskOne(prompt, &preset, nil)
@@ -39,26 +39,23 @@ func newEnvelope(name string) (*jess.Envelope, error) {
 
 	switch preset {
 	case "Encrypt with password":
-		envelope.Tools = jess.RecommendedStoragePassword
+		envelope.SuiteID = jess.SuitePassword
 		err = selectSignets(envelope, "pw")
-
-	case "Encrypt with keyfile":
-		envelope.Tools = jess.RecommendedStorageKey
+	case "Encrypt with key":
+		envelope.SuiteID = jess.SuiteKey
 		err = selectSignets(envelope, "key")
-
-	case "Encrypt for someone":
-		envelope.Tools = jess.RecommendedStorageKey
+	case "Encrypt for someone and sign":
+		envelope.SuiteID = jess.SuiteComplete
 		err = selectSignets(envelope, "recipient")
 		if err == nil {
 			err = selectSignets(envelope, "sender")
 		}
-
+	case "Encrypt for someone but don't sign":
+		envelope.SuiteID = jess.SuiteRcptOnly
+		err = selectSignets(envelope, "recipient")
 	case "Sign a file":
-		envelope.NoConfidentiality().NoIntegrity().NoRecipientAuth()
+		envelope.SuiteID = jess.SuiteSign
 		err = selectSignets(envelope, "sender")
-
-	case "Custom from scratch":
-		// do nothing
 	}
 	if err != nil {
 		return nil, err
@@ -72,12 +69,13 @@ func editEnvelope(envelope *jess.Envelope) error {
 		// main menu
 
 		// print envelope status
+		envelope.SecurityLevel = 0 // reset
 		session, err := envelope.Correspondence(trustStore)
 		if err != nil {
 			fmt.Printf("Envelope status: %s\n", err)
 		} else {
 			fmt.Println("Envelope status: valid.")
-			envelope.MinimumSecurityLevel = session.SecurityLevel
+			envelope.SecurityLevel = session.SecurityLevel
 		}
 
 		// sub menu
@@ -86,17 +84,18 @@ func editEnvelope(envelope *jess.Envelope) error {
 			Message: "Select to edit",
 			Options: formatColumns([][]string{
 				{"Done", "save and return"},
-				{" "},
-				{"Requirements", formatRequirements(envelope)},
-				{"Tools", strings.Join(envelope.Tools, ", ")},
+				{""},
+				{"Suite", envelope.SuiteID},
+				{"", "provides " + formatRequirements(envelope.Suite().Provides)},
+				{"", "and " + formatSecurityLevel(envelope.Suite().SecurityLevel)},
 				{"Secrets", formatSignetNames(envelope.Secrets)},
 				{"Recipients", formatSignetNames(envelope.Recipients)},
 				{"Senders", formatSignetNames(envelope.Senders)},
-				{" "},
+				{""},
 				{"Abort", "discard changes and return"},
 				{"Delete", "delete and return"},
 			}),
-			PageSize: 10,
+			PageSize: 15,
 		}
 		err = survey.AskOne(prompt, &submenu, nil)
 		if err != nil {
@@ -111,10 +110,8 @@ func editEnvelope(envelope *jess.Envelope) error {
 			return nil
 		case strings.HasPrefix(submenu, "Delete"):
 			return trustStore.DeleteEnvelope(envelope.Name)
-		case strings.HasPrefix(submenu, "Requirements"):
-			err = editEnvelopeRequirements(envelope)
-		case strings.HasPrefix(submenu, "Tools"):
-			err = editEnvelopeTools(envelope)
+		case strings.HasPrefix(submenu, "Suite"):
+			err = editEnvelopeSuite(envelope)
 		case strings.HasPrefix(submenu, "Secrets"):
 			err = selectSignets(envelope, "pw/key")
 		case strings.HasPrefix(submenu, "Recipients"):
@@ -128,69 +125,30 @@ func editEnvelope(envelope *jess.Envelope) error {
 	}
 }
 
-func editEnvelopeRequirements(envelope *jess.Envelope) error {
-	// TODO: improve
-
-	// get reqs
-	requirements := envelope.Requirements()
-	if requirements == nil {
-		return errors.New("envelope requirements uninitialized")
+func editEnvelopeSuite(envelope *jess.Envelope) error {
+	all := jess.Suites()
+	suiteOptions := make([][]string, 0, len(all))
+	for _, suite := range all {
+		suiteOptions = append(suiteOptions, []string{
+			suite.ID,
+			"provides " + suite.Provides.ShortString(),
+			formatSecurityLevel(suite.SecurityLevel),
+			"uses " + strings.Join(suite.Tools, ", "),
+			formatSuiteStatus(suite),
+		})
 	}
 
-	// build defaults
-	var defaults []string
-	if requirements.Has(jess.Confidentiality) {
-		defaults = append(defaults, "Confidentiality")
+	var selectedSuite string
+	prompt := &survey.Select{
+		Message:  "Select suite",
+		Options:  formatColumns(suiteOptions),
+		PageSize: 10,
 	}
-	if requirements.Has(jess.Integrity) {
-		defaults = append(defaults, "Integrity")
-	}
-	if requirements.Has(jess.RecipientAuthentication) {
-		defaults = append(defaults, "Recipient Authentication")
-	}
-	if requirements.Has(jess.SenderAuthentication) {
-		defaults = append(defaults, "Sender Authentication")
-	}
-
-	// prompt
-	var selected []string
-	prompt := &survey.MultiSelect{
-		Message: "Select requirements:",
-		Options: []string{
-			"Confidentiality",
-			"Integrity",
-			"Recipient Authentication",
-			"Sender Authentication",
-		},
-		Default: defaults,
-	}
-	err := survey.AskOne(prompt, &selected, nil)
+	err := survey.AskOne(prompt, &selectedSuite, nil)
 	if err != nil {
 		return err
 	}
 
-	// parse
-	requirements.Remove(jess.Confidentiality)
-	requirements.Remove(jess.Integrity)
-	requirements.Remove(jess.RecipientAuthentication)
-	requirements.Remove(jess.SenderAuthentication)
-	for _, req := range selected {
-		switch req {
-		case "Confidentiality":
-			requirements.Add(jess.Confidentiality)
-		case "Integrity":
-			requirements.Add(jess.Integrity)
-		case "Recipient Authentication":
-			requirements.Add(jess.RecipientAuthentication)
-		case "Sender Authentication":
-			requirements.Add(jess.SenderAuthentication)
-		}
-	}
-
-	return nil
-}
-
-func editEnvelopeTools(envelope *jess.Envelope) (err error) {
-	envelope.Tools, err = pickTools(envelope.Tools, "Select tools:")
-	return err
+	envelope.SuiteID = strings.Fields(selectedSuite)[0]
+	return envelope.ReloadSuite()
 }
diff --git a/cmd/cfg-signet.go b/cmd/cfg-signet.go
index cef217a..bf67988 100644
--- a/cmd/cfg-signet.go
+++ b/cmd/cfg-signet.go
@@ -2,7 +2,6 @@ package main
 
 import (
 	"errors"
-	"fmt"
 	"strings"
 	"time"
 
@@ -32,19 +31,8 @@ func newSignet(name, scheme string) (*jess.Signet, error) {
 		schemeSelection := [][]string{
 			{jess.SignetSchemePassword, "Password"},
 			{jess.SignetSchemeKey, "Key", "dynamic b/s (set manually via --symkeysize)"},
-		}
-		for _, tool := range tools.AsList() {
-			// check if tool support Signets
-			switch tool.Info.Purpose {
-			case tools.PurposeKeyExchange,
-				tools.PurposeKeyEncapsulation,
-				tools.PurposeSigning:
-				schemeSelection = append(schemeSelection, []string{
-					tool.Info.Name,
-					tool.Info.FormatPurpose(),
-					formatToolSecurityLevel(tool),
-				})
-			}
+			{"ECDH-X25519", "Receiving (KeyExchange)"},
+			{"Ed25519", "Signing"},
 		}
 
 		// select scheme
@@ -204,42 +192,6 @@ func selectSignets(envelope *jess.Envelope, scope string) error {
 		return err
 	}
 
-	// add schemes to envelope
-checkSchemaLoop:
-	for _, signet := range selectedSignets {
-		switch signet.Scheme {
-		case jess.SignetSchemePassword, jess.SignetSchemeKey:
-		default:
-			for _, toolID := range envelope.Tools {
-				scheme := toolID
-				if strings.Contains(scheme, "(") {
-					scheme = strings.Split(scheme, "(")[0]
-				}
-				if scheme == signet.Scheme {
-					continue checkSchemaLoop
-				}
-			}
-
-			// schema not found in envelope toolset
-			tool, err := signet.Tool()
-			if err != nil {
-				return err
-			}
-			if tool.Info.HasOption(tools.OptionNeedsManagedHasher) ||
-				tool.Info.HasOption(tools.OptionNeedsDedicatedHasher) {
-				// add hash tool
-				hashToolName, err := pickHashTool(fmt.Sprintf("Select hash tool for %s:", tool.Info.Name), tool.Info.SecurityLevel)
-				if err != nil {
-					return err
-				}
-				envelope.Tools = append(envelope.Tools, fmt.Sprintf("%s(%s)", tool.Info.Name, hashToolName))
-			} else {
-				envelope.Tools = append(envelope.Tools, signet.Scheme)
-			}
-
-		}
-	}
-
 	// make stubs
 	selectedSignetStubs := make([]*jess.Signet, 0, len(selectedSignets))
 	for _, signet := range selectedSignets {
diff --git a/cmd/cfg-tools.go b/cmd/cfg-tools.go
index 4267c25..d873ed5 100644
--- a/cmd/cfg-tools.go
+++ b/cmd/cfg-tools.go
@@ -11,7 +11,7 @@ import (
 	"github.com/AlecAivazis/survey"
 )
 
-func pickTools(toolNames []string, promptMsg string) ([]string, error) {
+func pickTools(toolNames []string, promptMsg string) ([]string, error) { //nolint:unused,deadcode // TODO
 	var toolSelection [][]string //nolint:prealloc
 	preSelectedTools := make([]string, 0, len(toolNames))
 	var preSelected int
@@ -101,7 +101,7 @@ func pickTools(toolNames []string, promptMsg string) ([]string, error) {
 	return newTools, nil
 }
 
-func pickHashTool(prompt string, minSecurityLevel int) (string, error) {
+func pickHashTool(prompt string, minSecurityLevel int) (string, error) { //nolint:unused // TODO
 	var hashToolSelection [][]string
 	for _, hashTool := range hashtools.AsList() {
 		if hashTool.SecurityLevel >= minSecurityLevel {
@@ -126,7 +126,7 @@ func pickHashTool(prompt string, minSecurityLevel int) (string, error) {
 	return strings.Fields(selectedEnty)[0], nil
 }
 
-func stringInSlice(s string, a []string) bool {
+func stringInSlice(s string, a []string) bool { //nolint:unused // TODO
 	for _, entry := range a {
 		if entry == s {
 			return true
diff --git a/cmd/cmd-close.go b/cmd/cmd-close.go
index 766f80e..a97ca7e 100644
--- a/cmd/cmd-close.go
+++ b/cmd/cmd-close.go
@@ -12,7 +12,7 @@ import (
 
 func init() {
 	rootCmd.AddCommand(closeCmd)
-	closeCmd.Flags().StringVarP(&closeFlagOutput, "output", "o", "", "specify output file")
+	closeCmd.Flags().StringVarP(&closeFlagOutput, "output", "o", "", "specify output file (`-` for stdout")
 }
 
 var (
@@ -20,9 +20,11 @@ var (
 	closeCmdHelp    = "usage: jess close <file> with <envelope name>"
 
 	closeCmd = &cobra.Command{
-		Use:     "close",
-		Short:   "encrypt file",
-		PreRunE: requireTrustStore,
+		Use:                   "close <file> with <envelope name>",
+		Short:                 "encrypt file",
+		Long:                  "encrypt file with the given envelope. Use `-` to use stdin",
+		DisableFlagsInUseLine: true,
+		PreRunE:               requireTrustStore,
 		RunE: func(cmd *cobra.Command, args []string) error {
 			registerPasswordCallbacks()
 
diff --git a/cmd/cmd-configure.go b/cmd/cmd-configure.go
index 8d2eec9..a1d03a8 100644
--- a/cmd/cmd-configure.go
+++ b/cmd/cmd-configure.go
@@ -16,10 +16,11 @@ func init() {
 
 var (
 	configureCmd = &cobra.Command{
-		Use:     "configure",
-		Short:   "configure (and create) envelope",
-		Args:    cobra.MaximumNArgs(1),
-		PreRunE: requireTrustStore,
+		Use:                   "configure <envelope name>",
+		Short:                 "configure (and create) envelope",
+		DisableFlagsInUseLine: true,
+		Args:                  cobra.MaximumNArgs(1),
+		PreRunE:               requireTrustStore,
 		RunE: func(cmd *cobra.Command, args []string) (err error) {
 			// check envelope name existence
 			if len(args) == 0 {
diff --git a/cmd/cmd-generate.go b/cmd/cmd-generate.go
index cdf9d43..e4f9b21 100644
--- a/cmd/cmd-generate.go
+++ b/cmd/cmd-generate.go
@@ -15,10 +15,11 @@ var (
 	generateFlagScheme string
 
 	generateCmd = &cobra.Command{
-		Use:     "generate",
-		Short:   "generate a new signet",
-		Args:    cobra.NoArgs,
-		PreRunE: requireTrustStore,
+		Use:                   "generate",
+		Short:                 "generate a new signet",
+		DisableFlagsInUseLine: true,
+		Args:                  cobra.NoArgs,
+		PreRunE:               requireTrustStore,
 		RunE: func(cmd *cobra.Command, args []string) error {
 			_, err := newSignet(generateFlagName, generateFlagScheme)
 			return err
diff --git a/cmd/cmd-tools.go b/cmd/cmd-list.go
similarity index 57%
rename from cmd/cmd-tools.go
rename to cmd/cmd-list.go
index c24192b..f2c68f8 100644
--- a/cmd/cmd-tools.go
+++ b/cmd/cmd-list.go
@@ -2,22 +2,42 @@ package main
 
 import (
 	"fmt"
+	"strings"
 
 	"github.com/spf13/cobra"
 
+	"github.com/safing/jess"
 	"github.com/safing/jess/hashtools"
 	"github.com/safing/jess/tools"
 )
 
 func init() {
-	rootCmd.AddCommand(toolsCmd)
+	rootCmd.AddCommand(listCmd)
 }
 
-var toolsCmd = &cobra.Command{
-	Use:   "tools",
-	Short: "list all available tools",
+var listCmd = &cobra.Command{
+	Use:                   "list",
+	Short:                 "list all available suites and tools",
+	DisableFlagsInUseLine: true,
 	Run: func(cmd *cobra.Command, args []string) {
-		fmt.Printf("Tools\n\n")
+		fmt.Printf("Suites\n\n")
+		suitesTable := [][]string{
+			{"Name/ID", "Provides", "Security Level", "Tools", "Notes"},
+		}
+		for _, suite := range jess.Suites() {
+			suitesTable = append(suitesTable, []string{
+				suite.ID,
+				suite.Provides.ShortString(),
+				formatSecurityLevel(suite.SecurityLevel),
+				strings.Join(suite.Tools, ", "),
+				formatSuiteStatus(suite),
+			})
+		}
+		for _, line := range formatColumns(suitesTable) {
+			fmt.Println(line)
+		}
+
+		fmt.Printf("\n\nTools\n\n")
 		toolTable := [][]string{
 			{"Name/ID", "Purpose", "Security Level", "Author", "Comment"},
 		}
@@ -34,7 +54,7 @@ var toolsCmd = &cobra.Command{
 			fmt.Println(line)
 		}
 
-		fmt.Printf("\nHashTools\n\n")
+		fmt.Printf("\n\nHashTools\n\n")
 		hashToolTable := [][]string{
 			{"Name/ID", "Security Level", "Author", "Comment"},
 		}
diff --git a/cmd/cmd-manage.go b/cmd/cmd-manage.go
index 5ac8e1f..deb232f 100644
--- a/cmd/cmd-manage.go
+++ b/cmd/cmd-manage.go
@@ -19,10 +19,11 @@ func init() {
 }
 
 var manageCmd = &cobra.Command{
-	Use:     "manage",
-	Short:   "manage a trust store",
-	Args:    cobra.MaximumNArgs(1),
-	PreRunE: requireTrustStore,
+	Use:                   "manage",
+	Short:                 "manage a trust store",
+	DisableFlagsInUseLine: true,
+	Args:                  cobra.MaximumNArgs(1),
+	PreRunE:               requireTrustStore,
 	RunE: func(cmd *cobra.Command, args []string) error {
 		// select action
 		var selectedAction string
@@ -117,7 +118,11 @@ func manageEnvelopes() error {
 		for _, envelope := range all {
 			selection = append(selection, []string{
 				envelope.Name,
-				envelope.Requirements().ShortString(),
+				envelope.SuiteID,
+				fmt.Sprintf("provides %s and %s",
+					envelope.Suite().Provides.ShortString(),
+					formatSecurityLevel(envelope.Suite().SecurityLevel),
+				),
 				formatEnvelopeSignets(envelope),
 			})
 		}
diff --git a/cmd/cmd-open.go b/cmd/cmd-open.go
index d78d216..97fedae 100644
--- a/cmd/cmd-open.go
+++ b/cmd/cmd-open.go
@@ -17,7 +17,7 @@ import (
 
 func init() {
 	rootCmd.AddCommand(openCmd)
-	openCmd.Flags().StringVarP(&openFlagOutput, "output", "o", "", "specify output file")
+	openCmd.Flags().StringVarP(&openFlagOutput, "output", "o", "", "specify output file (`-` for stdout")
 }
 
 var (
@@ -25,8 +25,9 @@ var (
 	openCmdHelp    = "usage: jess open <file>"
 
 	openCmd = &cobra.Command{
-		Use:   "open",
-		Short: "decrypt a file",
+		Use:   "open <file>",
+		Short: "decrypt file",
+		Long:  "decrypt file with the given envelope. Use `-` to use stdin",
 		RunE: func(cmd *cobra.Command, args []string) (err error) {
 			registerPasswordCallbacks()
 
@@ -39,7 +40,7 @@ var (
 			filename := args[0]
 			outputFilename := openFlagOutput
 			if outputFilename == "" {
-				if !strings.HasSuffix(filename, ".letter") || len(outputFilename) < 8 {
+				if !strings.HasSuffix(filename, ".letter") || len(filename) < 8 {
 					return errors.New("cannot automatically derive output filename, please specify with --output")
 				}
 				outputFilename = strings.TrimSuffix(filename, ".letter")
diff --git a/cmd/cmd-verify.go b/cmd/cmd-verify.go
index 1aaf0a8..f53311d 100644
--- a/cmd/cmd-verify.go
+++ b/cmd/cmd-verify.go
@@ -21,8 +21,9 @@ var (
 	verifyCmdHelp = "usage: jess verify <file>"
 
 	verifyCmd = &cobra.Command{
-		Use:   "verify",
-		Short: "verify a file",
+		Use:                   "verify <file>",
+		Short:                 "verify file",
+		DisableFlagsInUseLine: true,
 		RunE: func(cmd *cobra.Command, args []string) (err error) {
 			// check args
 			if len(args) != 1 {
diff --git a/cmd/format.go b/cmd/format.go
index f4adc10..d5b2b50 100644
--- a/cmd/format.go
+++ b/cmd/format.go
@@ -38,6 +38,10 @@ func formatColumns(table [][]string) []string {
 	return lines
 }
 
+func formatSecurityLevel(securityLevel int) string {
+	return fmt.Sprintf("%d b/s", securityLevel)
+}
+
 func formatToolSecurityLevel(tool *tools.Tool) string {
 	if tool.Info.HasOption(tools.OptionNeedsSecurityLevel) {
 		return "dynamic b/s (set manually via --seclevel)"
@@ -45,7 +49,7 @@ func formatToolSecurityLevel(tool *tools.Tool) string {
 	if tool.Info.SecurityLevel == 0 {
 		return ""
 	}
-	return fmt.Sprintf("%d b/s", tool.Info.SecurityLevel)
+	return formatSecurityLevel(tool.Info.SecurityLevel)
 }
 
 func formatSignetName(signet *jess.Signet) string {
@@ -116,12 +120,11 @@ func formatSignetSecurityLevel(signet *jess.Signet) string {
 	return fmt.Sprintf("%d b/s", securityLevel)
 }
 
-func formatRequirements(envelope *jess.Envelope) string {
-	attrs := envelope.Requirements()
-	if attrs == nil || attrs.Empty() {
+func formatRequirements(reqs *jess.Requirements) string {
+	if reqs == nil || reqs.Empty() {
 		return "none (unsafe)"
 	}
-	return attrs.String()
+	return reqs.String()
 }
 
 func formatSignetNames(signets []*jess.Signet) string {
@@ -145,3 +148,14 @@ func formatEnvelopeSignets(envelope *jess.Envelope) string {
 	}
 	return strings.Join(sections, ", ")
 }
+
+func formatSuiteStatus(suite *jess.Suite) string {
+	switch suite.Status {
+	case jess.SuiteStatusDeprecated:
+		return "DEPRECATED"
+	case jess.SuiteStatusRecommended:
+		return "recommended"
+	default:
+		return ""
+	}
+}