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 "" + } +}