From d4d06574b80a874fca0f2eaf2f050fead33b401b Mon Sep 17 00:00:00 2001 From: Daniel <dhaavi@users.noreply.github.com> Date: Mon, 8 Aug 2022 14:45:06 +0200 Subject: [PATCH] Add text format for signets and envelopes and support for import/export --- cmd/cfg-envelope.go | 28 ++- ...c84c-78f7-4354-a7f5-0e115aa2903c.recipient | 14 ++ ...911c84c-78f7-4354-a7f5-0e115aa2903c.signet | 13 ++ .../.truststore/safing-codesign-1.envelope | 23 +++ cmd/testdata/test.txt | 1 + cmd/testdata/test.txt.letter | 13 ++ cmd/testdata/test.txt.sig | 9 + cmd/testdata/test3.txt | 1 + cmd/testdata/test3.txt.sig | 9 + cmd/testdata/test4.txt | 1 + cmd/testdata/testdir/test2.txt | 1 + cmd/testdata/testdir/test2.txt.sig | 9 + envelope.go | 81 ++++++++ import_export.go | 192 ++++++++++++++++++ signet.go | 8 + 15 files changed, 393 insertions(+), 10 deletions(-) create mode 100644 cmd/testdata/.truststore/3911c84c-78f7-4354-a7f5-0e115aa2903c.recipient create mode 100644 cmd/testdata/.truststore/3911c84c-78f7-4354-a7f5-0e115aa2903c.signet create mode 100644 cmd/testdata/.truststore/safing-codesign-1.envelope create mode 100644 cmd/testdata/test.txt create mode 100644 cmd/testdata/test.txt.letter create mode 100644 cmd/testdata/test.txt.sig create mode 100644 cmd/testdata/test3.txt create mode 100644 cmd/testdata/test3.txt.sig create mode 100644 cmd/testdata/test4.txt create mode 100644 cmd/testdata/testdir/test2.txt create mode 100644 cmd/testdata/testdir/test2.txt.sig create mode 100644 import_export.go diff --git a/cmd/cfg-envelope.go b/cmd/cfg-envelope.go index 8a58686..873ac6b 100644 --- a/cmd/cfg-envelope.go +++ b/cmd/cfg-envelope.go @@ -30,7 +30,7 @@ func newEnvelope(name string) (*jess.Envelope, error) { "Encrypt with key", "Encrypt for someone and sign", "Encrypt for someone but don't sign", - "Sign a file", + "Sign a file (wrapped)", }, } err := survey.AskOne(prompt, &preset, nil) @@ -93,6 +93,7 @@ func editEnvelope(envelope *jess.Envelope) error { {"Recipients", formatSignetNames(envelope.Recipients)}, {"Senders", formatSignetNames(envelope.Senders)}, {""}, + {"Export", "export to text format"}, {"Abort", "discard changes and return"}, {"Delete", "delete and return"}, }), @@ -105,21 +106,28 @@ func editEnvelope(envelope *jess.Envelope) error { switch { case strings.HasPrefix(submenu, "Done"): - // Check if the envolope is valid. + // Check if the envelope is valid. if envelope.SecurityLevel == 0 { fmt.Println("Envelope is invalid, please fix before saving.") continue } // Remove and keys and save. - _ = envelope.LoopSecrets("", func(signet *jess.Signet) error { - signet.Key = nil - return nil - }) - _ = envelope.LoopSenders("", func(signet *jess.Signet) error { - signet.Key = nil - return nil - }) + envelope.CleanSignets() return trustStore.StoreEnvelope(envelope) + case strings.HasPrefix(submenu, "Export"): + // Check if the envelope is valid. + if envelope.SecurityLevel == 0 { + fmt.Println("Envelope is invalid, please fix before exporting.") + continue + } + // Remove keys and export. + envelope.CleanSignets() + text, err := envelope.Export(false) + if err != nil { + return fmt.Errorf("failed to export: %w", err) + } + fmt.Println("Exported envelope:") + fmt.Println(text) case strings.HasPrefix(submenu, "Abort"): return nil case strings.HasPrefix(submenu, "Delete"): diff --git a/cmd/testdata/.truststore/3911c84c-78f7-4354-a7f5-0e115aa2903c.recipient b/cmd/testdata/.truststore/3911c84c-78f7-4354-a7f5-0e115aa2903c.recipient new file mode 100644 index 0000000..1958281 --- /dev/null +++ b/cmd/testdata/.truststore/3911c84c-78f7-4354-a7f5-0e115aa2903c.recipient @@ -0,0 +1,14 @@ +J{ + "Version": 1, + "ID": "3911c84c-78f7-4354-a7f5-0e115aa2903c", + "Scheme": "Ed25519", + "Key": "ATYVZjmhR1Zwe0KAPV99pzbzI+6zWgKvELNhFwolRdnv", + "Public": true, + "Info": { + "Name": "Safing Code Signing Cert 1", + "Owner": "", + "Created": "2022-07-11T10:23:31.705715613+02:00", + "Expires": "0001-01-01T00:00:00Z", + "Details": null + } +} \ No newline at end of file diff --git a/cmd/testdata/.truststore/3911c84c-78f7-4354-a7f5-0e115aa2903c.signet b/cmd/testdata/.truststore/3911c84c-78f7-4354-a7f5-0e115aa2903c.signet new file mode 100644 index 0000000..ef9764d --- /dev/null +++ b/cmd/testdata/.truststore/3911c84c-78f7-4354-a7f5-0e115aa2903c.signet @@ -0,0 +1,13 @@ +J{ + "Version": 1, + "ID": "3911c84c-78f7-4354-a7f5-0e115aa2903c", + "Scheme": "Ed25519", + "Key": "Aee5n/V1wJM8aNpaNEPBEPeN6S0Tl41OJP0rHwtsGcZcNhVmOaFHVnB7QoA9X32nNvMj7rNaAq8Qs2EXCiVF2e8=", + "Info": { + "Name": "Safing Code Signing Cert 1", + "Owner": "", + "Created": "2022-07-11T10:23:31.705715613+02:00", + "Expires": "0001-01-01T00:00:00Z", + "Details": null + } +} \ No newline at end of file diff --git a/cmd/testdata/.truststore/safing-codesign-1.envelope b/cmd/testdata/.truststore/safing-codesign-1.envelope new file mode 100644 index 0000000..d853b67 --- /dev/null +++ b/cmd/testdata/.truststore/safing-codesign-1.envelope @@ -0,0 +1,23 @@ +J{ + "Version": 1, + "Name": "safing-codesign-1", + "SuiteID": "signfile_v1", + "Secrets": null, + "Senders": [ + { + "Version": 1, + "ID": "3911c84c-78f7-4354-a7f5-0e115aa2903c", + "Scheme": "Ed25519", + "Key": null, + "Info": { + "Name": "Safing Code Signing Cert 1", + "Owner": "", + "Created": "2022-07-11T10:23:31.705715613+02:00", + "Expires": "0001-01-01T00:00:00Z", + "Details": null + } + } + ], + "Recipients": null, + "SecurityLevel": 128 +} \ No newline at end of file diff --git a/cmd/testdata/test.txt b/cmd/testdata/test.txt new file mode 100644 index 0000000..a042389 --- /dev/null +++ b/cmd/testdata/test.txt @@ -0,0 +1 @@ +hello world! diff --git a/cmd/testdata/test.txt.letter b/cmd/testdata/test.txt.letter new file mode 100644 index 0000000..0646238 --- /dev/null +++ b/cmd/testdata/test.txt.letter @@ -0,0 +1,13 @@ +�J{ + "Version": 1, + "SuiteID": "signfile_v1", + "Nonce": "pKOQBQ==", + "Signatures": [ + { + "Scheme": "Ed25519", + "ID": "3911c84c-78f7-4354-a7f5-0e115aa2903c", + "Value": "ftsIINZ9oApKiXYQTcLIdAZDSflp6nRN/y8Gm0rdQC+3/wal6Q+7N3N8HEAxpoxWseSQNaRVCT9hSnRQStHYBA==" + } + ] +} + hello world! diff --git a/cmd/testdata/test.txt.sig b/cmd/testdata/test.txt.sig new file mode 100644 index 0000000..4eaab30 --- /dev/null +++ b/cmd/testdata/test.txt.sig @@ -0,0 +1,9 @@ +-----BEGIN JESS SIGNATURE----- +Q6VnVmVyc2lvbgFnU3VpdGVJRGtzaWduZmlsZV92MWVOb25jZUQb+MqAZERhdGFY +d02Dq0xhYmVsZWRIYXNoxCIJIOz3Afcn2eLXfEqkmsb7vMmXJ4rKAQvd7rlhwQz1 +TUNaqFNpZ25lZEF01v9iy+uLqE1ldGFEYXRhgqd2ZXJzaW9upTAuMC4xqmlkZW50 +aWZpZXKyd2luZG93cy9jb2RlL3RoaW5nalNpZ25hdHVyZXOBo2ZTY2hlbWVnRWQy +NTUxOWJJRHgkMzkxMWM4NGMtNzhmNy00MzU0LWE3ZjUtMGUxMTVhYTI5MDNjZVZh +bHVlWECJZFbIifczUGAJkmATXCHy/MiQZkiktM99X7U/cPgw3IKpKAxQsJ5LobgZ +4P2ecv0IlN4gQb+x+lycxl93E9sJ +-----END JESS SIGNATURE----- \ No newline at end of file diff --git a/cmd/testdata/test3.txt b/cmd/testdata/test3.txt new file mode 100644 index 0000000..25c2c9e --- /dev/null +++ b/cmd/testdata/test3.txt @@ -0,0 +1 @@ +hello world!! diff --git a/cmd/testdata/test3.txt.sig b/cmd/testdata/test3.txt.sig new file mode 100644 index 0000000..f654070 --- /dev/null +++ b/cmd/testdata/test3.txt.sig @@ -0,0 +1,9 @@ +-----BEGIN JESS SIGNATURE----- +Q6VnVmVyc2lvbgFnU3VpdGVJRGtzaWduZmlsZV92MWVOb25jZUQJ9s/nZERhdGFY +d02Dq0xhYmVsZWRIYXNoxCIJILtKnL1AHj7YubrWdLu1D+voud8Ky04vh756eTae +rWQwqFNpZ25lZEF01v9izC6hqE1ldGFEYXRhgqd2ZXJzaW9upTAuMC4xqmlkZW50 +aWZpZXKyd2luZG93cy9jb2RlL3RoaW5nalNpZ25hdHVyZXOBo2ZTY2hlbWVnRWQy +NTUxOWJJRHgkMzkxMWM4NGMtNzhmNy00MzU0LWE3ZjUtMGUxMTVhYTI5MDNjZVZh +bHVlWEBLsd2QbM7VmEsnW60hHn/V6EP2mGFauWZgbEOlKTiqumVFbWU4K7Fi91KL +Zgvwj+CNdZJ7Xv2qR7etviRDCmwC +-----END JESS SIGNATURE----- \ No newline at end of file diff --git a/cmd/testdata/test4.txt b/cmd/testdata/test4.txt new file mode 100644 index 0000000..a042389 --- /dev/null +++ b/cmd/testdata/test4.txt @@ -0,0 +1 @@ +hello world! diff --git a/cmd/testdata/testdir/test2.txt b/cmd/testdata/testdir/test2.txt new file mode 100644 index 0000000..a042389 --- /dev/null +++ b/cmd/testdata/testdir/test2.txt @@ -0,0 +1 @@ +hello world! diff --git a/cmd/testdata/testdir/test2.txt.sig b/cmd/testdata/testdir/test2.txt.sig new file mode 100644 index 0000000..524d2b7 --- /dev/null +++ b/cmd/testdata/testdir/test2.txt.sig @@ -0,0 +1,9 @@ +-----BEGIN JESS SIGNATURE----- +Q6VnVmVyc2lvbgFnU3VpdGVJRGtzaWduZmlsZV92MWVOb25jZUThzxO6ZERhdGFY +d02Dq0xhYmVsZWRIYXNoxCIJIOz3Afcn2eLXfEqkmsb7vMmXJ4rKAQvd7rlhwQz1 +TUNaqFNpZ25lZEF01v9izC3SqE1ldGFEYXRhgqd2ZXJzaW9upTAuMC4xqmlkZW50 +aWZpZXKyd2luZG93cy9jb2RlL3RoaW5nalNpZ25hdHVyZXOBo2ZTY2hlbWVnRWQy +NTUxOWJJRHgkMzkxMWM4NGMtNzhmNy00MzU0LWE3ZjUtMGUxMTVhYTI5MDNjZVZh +bHVlWEAGLkIoej0+ilJrIyb+BzX8+Yw2LY0zkoL9vwI02/2KqKVT7/pH+LTDX1Hl +h1epYkF8ICdwa1iVNDx6P7iNmWkL +-----END JESS SIGNATURE----- \ No newline at end of file diff --git a/envelope.go b/envelope.go index 48d0430..2935041 100644 --- a/envelope.go +++ b/envelope.go @@ -3,6 +3,10 @@ package jess import ( "errors" "fmt" + + "github.com/mr-tron/base58" + + "github.com/safing/portbase/formats/dsd" ) // Envelope holds configuration for jess to put data into a letter. @@ -268,3 +272,80 @@ func fillPassword(signet *Signet, createPassword bool, storage TrustStore, minSe } return getPasswordCallback(signet) } + +// CleanSignets cleans all the signets from all the non-necessary data as well +// as key material. +// This is for preparing for serializing and saving the signet. +func (e *Envelope) CleanSignets() { + for i, signet := range e.Secrets { + e.Secrets[i] = &Signet{ + Version: signet.Version, + ID: signet.ID, + Scheme: signet.Scheme, + } + } + for i, signet := range e.Senders { + e.Secrets[i] = &Signet{ + Version: signet.Version, + ID: signet.ID, + Scheme: signet.Scheme, + } + } + for i, signet := range e.Recipients { + e.Secrets[i] = &Signet{ + Version: signet.Version, + ID: signet.ID, + Scheme: signet.Scheme, + } + } +} + +// ToBytes serializes the envelope to a byte slice. +func (e *Envelope) ToBytes() ([]byte, error) { + // Minimize data and remove any key material. + e.CleanSignets() + + // Serialize envelope. + data, err := dsd.Dump(e, dsd.CBOR) + if err != nil { + return nil, fmt.Errorf("failed to serialize the envelope: %w", err) + } + + return data, nil +} + +// EnvelopeFromBytes parses and loads a serialized envelope. +func EnvelopeFromBytes(data []byte) (*Envelope, error) { + e := &Envelope{} + + // Parse envelope from data. + if _, err := dsd.Load(data, e); err != nil { + return nil, fmt.Errorf("failed to parse data format: %w", err) + } + + return e, nil +} + +// ToBase58 serializes the envelope and encodes it with base58. +func (e *Envelope) ToBase58() (string, error) { + // Serialize Signet. + data, err := e.ToBytes() + if err != nil { + return "", err + } + + // Encode and return. + return base58.Encode(data), nil +} + +// EnvelopeFromBase58 parses and loads a base58 encoded serialized envelope. +func EnvelopeFromBase58(base58Encoded string) (*Envelope, error) { + // Decode string. + data, err := base58.Decode(base58Encoded) + if err != nil { + return nil, fmt.Errorf("failed to decode base58: %w", err) + } + + // Parse and return. + return EnvelopeFromBytes(data) +} diff --git a/import_export.go b/import_export.go new file mode 100644 index 0000000..347d5c2 --- /dev/null +++ b/import_export.go @@ -0,0 +1,192 @@ +package jess + +import ( + "errors" + "fmt" + "regexp" + "strings" +) + +const ( + ExportSenderKeyword = "sender" + ExportSenderPrefix = "sender:" + + ExportRecipientKeyword = "recipient" + ExportRecipientPrefix = "recipient:" + + ExportKeyKeyword = "secret" + ExportKeyPrefix = "secret:" + + ExportEnvelopeKeyword = "envelope" + ExportEnvelopePrefix = "envelope:" +) + +// Export exports the public part of a signet in text format. +func (signet *Signet) Export(short bool) (textFormat string, err error) { + // Make public if needed. + if !signet.Public { + signet, err = signet.AsRecipient() + if err != nil { + return "", err + } + } + + // Transform to text format. + return signet.toTextFormat(short) +} + +// Backup exports the private part of a signet in text format. +func (signet *Signet) Backup(short bool) (textFormat string, err error) { + // Abprt if public. + if signet.Public { + return "", errors.New("cannot backup (only export) a recipient") + } + + // Transform to text format. + return signet.toTextFormat(short) +} + +func (signet *Signet) toTextFormat(short bool) (textFormat string, err error) { + // Serialize to base58. + base58data, err := signet.ToBase58() + if err != nil { + return "", err + } + + // Define keywords. + var keyword, typeComment string + switch { + case signet.Scheme == SignetSchemePassword: + return "", errors.New("cannot backup or export passwords") + case signet.Scheme == SignetSchemeKey: + // Check if the signet is marked as "public". + if signet.Public { + return "", errors.New("cannot export keys") + } + keyword = ExportKeyKeyword + typeComment = "symmetric-key" + case signet.Public: + keyword = ExportRecipientKeyword + typeComment = fmt.Sprintf( + "public-%s-key", toTextFormatString(signet.Scheme), + ) + default: + keyword = ExportSenderKeyword + typeComment = fmt.Sprintf( + "private-%s-key", toTextFormatString(signet.Scheme), + ) + } + + // Transform to text format. + if short { + return fmt.Sprintf( + "%s:%s", + keyword, + base58data, + ), nil + } + return fmt.Sprintf( + "%s:%s:%s:%s", + keyword, + typeComment, + toTextFormatString(signet.Info.Name), + base58data, + ), nil +} + +// Export exports the envelope in text format. +func (e *Envelope) Export(short bool) (textFormat string, err error) { + // Remove and key data. + e.CleanSignets() + + // Serialize to base58. + base58data, err := e.ToBase58() + if err != nil { + return "", err + } + + // Transform to text format. + if short { + return fmt.Sprintf( + "%s:%s", + ExportEnvelopeKeyword, + base58data, + ), nil + } + return fmt.Sprintf( + "%s:%s:%s:%s", + ExportEnvelopeKeyword, + e.SuiteID, + e.Name, + base58data, + ), nil +} + +// KeyFromTextFormat loads a secret key from the text format. +func KeyFromTextFormat(textFormat string) (*Signet, error) { + // Check the identifier. + if !strings.HasPrefix(textFormat, ExportKeyPrefix) { + return nil, errors.New("not a secret") + } + + // Parse the data section. + splitted := strings.Split(textFormat, ":") + if len(splitted) < 2 { + return nil, errors.New("invalid format") + } + return SignetFromBase58(splitted[len(splitted)-1]) +} + +// SenderFromTextFormat loads a sender (private key) from the text format. +func SenderFromTextFormat(textFormat string) (*Signet, error) { + // Check the identifier. + if !strings.HasPrefix(textFormat, ExportSenderPrefix) { + return nil, errors.New("not a sender") + } + + // Parse the data section. + splitted := strings.Split(textFormat, ":") + if len(splitted) < 2 { + return nil, errors.New("invalid format") + } + return SignetFromBase58(splitted[len(splitted)-1]) +} + +// RecipientFromTextFormat loads a recipient (public key) from the text format. +func RecipientFromTextFormat(textFormat string) (*Signet, error) { + // Check the identifier. + if !strings.HasPrefix(textFormat, ExportRecipientPrefix) { + return nil, errors.New("not a recipient") + } + + // Parse the data section. + splitted := strings.Split(textFormat, ":") + if len(splitted) < 2 { + return nil, errors.New("invalid format") + } + return SignetFromBase58(splitted[len(splitted)-1]) +} + +// EnvelopeFromTextFormat loads an envelope from the text format. +func EnvelopeFromTextFormat(textFormat string) (*Envelope, error) { + // Check the identifier. + if !strings.HasPrefix(textFormat, ExportEnvelopePrefix) { + return nil, errors.New("not an envelope") + } + + // Parse the data section. + splitted := strings.Split(textFormat, ":") + if len(splitted) < 2 { + return nil, errors.New("invalid format") + } + return EnvelopeFromBase58(splitted[len(splitted)-1]) +} + +var replaceForTextFormatMatcher = regexp.MustCompile(`[^A-Za-z\-]+`) + +// toTextFormatString makes a string compatible with the text format. +func toTextFormatString(s string) string { + return strings.ToLower( + replaceForTextFormatMatcher.ReplaceAllString(s, "_"), + ) +} diff --git a/signet.go b/signet.go index 367abf4..9a950d8 100644 --- a/signet.go +++ b/signet.go @@ -136,6 +136,14 @@ func (signet *Signet) SetLoadedKeys(pubKey crypto.PublicKey, privKey crypto.Priv // AsRecipient returns a public version of the Signet. func (signet *Signet) AsRecipient() (*Signet, error) { + // Check special signet schemes. + switch signet.Scheme { + case SignetSchemeKey: + return nil, errors.New("keys cannot be a recipient") + case SignetSchemePassword: + return nil, errors.New("passwords cannot be a recipient") + } + // load so we can split keys err := signet.LoadKey() if err != nil {