Add text format for signets and envelopes and support for import/export

This commit is contained in:
Daniel 2022-08-08 14:45:06 +02:00
parent 74d41194cc
commit d4d06574b8
15 changed files with 393 additions and 10 deletions

View file

@ -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"):

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}

1
cmd/testdata/test.txt vendored Normal file
View file

@ -0,0 +1 @@
hello world!

13
cmd/testdata/test.txt.letter vendored Normal file
View file

@ -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!

9
cmd/testdata/test.txt.sig vendored Normal file
View file

@ -0,0 +1,9 @@
-----BEGIN JESS SIGNATURE-----
Q6VnVmVyc2lvbgFnU3VpdGVJRGtzaWduZmlsZV92MWVOb25jZUQb+MqAZERhdGFY
d02Dq0xhYmVsZWRIYXNoxCIJIOz3Afcn2eLXfEqkmsb7vMmXJ4rKAQvd7rlhwQz1
TUNaqFNpZ25lZEF01v9iy+uLqE1ldGFEYXRhgqd2ZXJzaW9upTAuMC4xqmlkZW50
aWZpZXKyd2luZG93cy9jb2RlL3RoaW5nalNpZ25hdHVyZXOBo2ZTY2hlbWVnRWQy
NTUxOWJJRHgkMzkxMWM4NGMtNzhmNy00MzU0LWE3ZjUtMGUxMTVhYTI5MDNjZVZh
bHVlWECJZFbIifczUGAJkmATXCHy/MiQZkiktM99X7U/cPgw3IKpKAxQsJ5LobgZ
4P2ecv0IlN4gQb+x+lycxl93E9sJ
-----END JESS SIGNATURE-----

1
cmd/testdata/test3.txt vendored Normal file
View file

@ -0,0 +1 @@
hello world!!

9
cmd/testdata/test3.txt.sig vendored Normal file
View file

@ -0,0 +1,9 @@
-----BEGIN JESS SIGNATURE-----
Q6VnVmVyc2lvbgFnU3VpdGVJRGtzaWduZmlsZV92MWVOb25jZUQJ9s/nZERhdGFY
d02Dq0xhYmVsZWRIYXNoxCIJILtKnL1AHj7YubrWdLu1D+voud8Ky04vh756eTae
rWQwqFNpZ25lZEF01v9izC6hqE1ldGFEYXRhgqd2ZXJzaW9upTAuMC4xqmlkZW50
aWZpZXKyd2luZG93cy9jb2RlL3RoaW5nalNpZ25hdHVyZXOBo2ZTY2hlbWVnRWQy
NTUxOWJJRHgkMzkxMWM4NGMtNzhmNy00MzU0LWE3ZjUtMGUxMTVhYTI5MDNjZVZh
bHVlWEBLsd2QbM7VmEsnW60hHn/V6EP2mGFauWZgbEOlKTiqumVFbWU4K7Fi91KL
Zgvwj+CNdZJ7Xv2qR7etviRDCmwC
-----END JESS SIGNATURE-----

1
cmd/testdata/test4.txt vendored Normal file
View file

@ -0,0 +1 @@
hello world!

1
cmd/testdata/testdir/test2.txt vendored Normal file
View file

@ -0,0 +1 @@
hello world!

9
cmd/testdata/testdir/test2.txt.sig vendored Normal file
View file

@ -0,0 +1,9 @@
-----BEGIN JESS SIGNATURE-----
Q6VnVmVyc2lvbgFnU3VpdGVJRGtzaWduZmlsZV92MWVOb25jZUThzxO6ZERhdGFY
d02Dq0xhYmVsZWRIYXNoxCIJIOz3Afcn2eLXfEqkmsb7vMmXJ4rKAQvd7rlhwQz1
TUNaqFNpZ25lZEF01v9izC3SqE1ldGFEYXRhgqd2ZXJzaW9upTAuMC4xqmlkZW50
aWZpZXKyd2luZG93cy9jb2RlL3RoaW5nalNpZ25hdHVyZXOBo2ZTY2hlbWVnRWQy
NTUxOWJJRHgkMzkxMWM4NGMtNzhmNy00MzU0LWE3ZjUtMGUxMTVhYTI5MDNjZVZh
bHVlWEAGLkIoej0+ilJrIyb+BzX8+Yw2LY0zkoL9vwI02/2KqKVT7/pH+LTDX1Hl
h1epYkF8ICdwa1iVNDx6P7iNmWkL
-----END JESS SIGNATURE-----

View file

@ -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)
}

192
import_export.go Normal file
View file

@ -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, "_"),
)
}

View file

@ -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 {