diff --git a/keys.go b/keys.go new file mode 100644 index 0000000..03cf8ca --- /dev/null +++ b/keys.go @@ -0,0 +1,70 @@ +package paseto + +// keyPurpose indicates if key is symmetric, private or public +type keyPurpose int + +// keyVersion indicates the token version the key may be used for +type KeyVersion int + +const ( + // Invalid key version + KeyVersionInvalid KeyVersion = 0 + // Key used for V1 tokens + KeyVersionV1 KeyVersion = 1 + // Key used for V2 tokens + KeyVersionV2 KeyVersion = 2 + // Key used for V3 tokens + KeyVersionV3 KeyVersion = 3 + // Key used for V4 tokens + KeyVersionV4 KeyVersion = 4 + + // Symmetric key used for local tokens + keyPurposeLocal keyPurpose = 1 + // Asymmetric secret key used for public tokens + keyPurposeSecret keyPurpose = 2 + // Asymmetric public key used for public tokens + keyPurposePublic keyPurpose = 3 +) + +type Key interface { + // Export raw key data as hex string + ExportHex() string + // Export raw key data as byte array + ExportBytes() []byte + // Returns purpose of key + getPurpose() keyPurpose + // Returns the version of the paseto tokens the key may be used for + getVersion() KeyVersion +} + +// Convert key version to PASERK header string +func KeyVersionToString(version KeyVersion) string { + switch version { + case KeyVersionV1: + return "k1" + case KeyVersionV2: + return "k2" + case KeyVersionV3: + return "k3" + case KeyVersionV4: + return "k4" + default: + return "" + } +} + +// Parse key version from PASERK header string (eg. "k2") +func KeyVersionFromString(versionStr string) KeyVersion { + switch versionStr { + case "k1": + return KeyVersionV1 + case "k2": + return KeyVersionV2 + case "k3": + return KeyVersionV3 + case "k4": + return KeyVersionV4 + default: + return KeyVersionInvalid + } +} diff --git a/paserk.go b/paserk.go new file mode 100644 index 0000000..5ba01ba --- /dev/null +++ b/paserk.go @@ -0,0 +1,209 @@ +package paseto + +import ( + "encoding/base64" + "fmt" + "strings" + + "github.com/pkg/errors" +) + +// type of PASERK token +type PaserkType int + +// Error indicating the given paserk import/export method is not yet implemented on the key type. +type NotImplementedError struct { + keyTypeStr string + paserkTypeStr string +} + +// Error indicating the given paserk type is not valid for the key type. +type InvalidPaserkTypeError struct { + keyTypeStr string + paserkTypeStr string +} + +const ( + // Invalid Paserk token type + PaserkTypeInvalid PaserkType = 0 + // Unique Identifier for a separate PASERK for local PASETOs + PaserkTypeLid PaserkType = 1 + // Symmetric key for local tokens + PaserkTypeLocal PaserkType = 2 + // Symmetric key wrapped using asymmetric encryption + PaserkTypeSeal PaserkType = 3 + // Symmetric key wrapped by another symmetric key + PaserkTypeLocalWrap PaserkType = 4 + // Symmetric key wrapped using password-based encryption + PaserkTypeLocalPw PaserkType = 5 + // Unique Identifier for a separate PASERK for public PASETOs. (Secret Key) + PaserkTypeSid PaserkType = 6 + // Public key for verifying public tokens + PaserkTypePublic PaserkType = 7 + // Unique Identifier for a separate PASERK for public PASETOs. (Public Key) + PaserkTypePid PaserkType = 8 + // Secret key for signing public tokens + PaserkTypeSecret PaserkType = 9 + // Asymmetric secret key wrapped by another symmetric key + PaserkTypeSecretWrap PaserkType = 10 + // Asymmetric secret key wrapped using password-based encryption + PaserkTypeSecretPw PaserkType = 11 +) + +// Convert token type to PASERK header string `type` +func PaserkTypeToString(paserkType PaserkType) string { + switch paserkType { + case PaserkTypeLid: + return "lid" + case PaserkTypeLocal: + return "local" + case PaserkTypeSeal: + return "seal" + case PaserkTypeLocalWrap: + return "local-wrap" + case PaserkTypeLocalPw: + return "local-pw" + case PaserkTypeSid: + return "sid" + case PaserkTypePublic: + return "public" + case PaserkTypePid: + return "pid" + case PaserkTypeSecret: + return "secret" + case PaserkTypeSecretWrap: + return "secret-wrap" + case PaserkTypeSecretPw: + return "secret-pw" + default: + return "" + } +} + +// Parse token type from string value of `type` field of PASERK token +func PaserkTypeFromString(typeStr string) PaserkType { + switch typeStr { + case "lid": + return PaserkTypeLid + case "local": + return PaserkTypeLocal + case "seal": + return PaserkTypeSeal + case "local-wrap": + return PaserkTypeLocalWrap + case "local-pw": + return PaserkTypeLocalPw + case "sid": + return PaserkTypeSid + case "public": + return PaserkTypePublic + case "pid": + return PaserkTypePid + case "secret": + return PaserkTypeSecret + case "secret-wrap": + return PaserkTypeSecretWrap + case "secret-pw": + return PaserkTypeSecretPw + default: + return PaserkTypeInvalid + } +} + +// Checks if the representation (paserk token type) is available for the key +func (paserkType PaserkType) isAvailableForKey(key Key) bool { + switch key.getPurpose() { + case keyPurposeLocal: + switch paserkType { + case PaserkTypeLid, + PaserkTypeLocal, + PaserkTypeSeal, + PaserkTypeLocalWrap, + PaserkTypeLocalPw: + return true + default: + return false + } + case keyPurposePublic: + switch paserkType { + case PaserkTypePid, + PaserkTypePublic: + return true + default: + return false + } + case keyPurposeSecret: + switch paserkType { + case PaserkTypeSid, + PaserkTypeSecret, + PaserkTypeSecretWrap, + PaserkTypeSecretPw: + return true + default: + return false + } + default: + return false + } +} + +func (e NotImplementedError) Error() string { + return fmt.Sprintf("PASERK type %s is not yet implemented on key type %s", e.paserkTypeStr, e.keyTypeStr) +} + +func (e InvalidPaserkTypeError) Error() string { + return fmt.Sprintf("PASERK type %s is invalid for key type %s", e.paserkTypeStr, e.keyTypeStr) +} + +// ExportPaserk export a V4AsymmetricPublicKey to a paserk token of type paserkType +func ExportPaserkRaw(k Key) (string, error) { + var paserkType PaserkType + switch k.getPurpose() { + case keyPurposeLocal: + paserkType = PaserkTypeLocal + case keyPurposeSecret: + paserkType = PaserkTypeSecret + case keyPurposePublic: + paserkType = PaserkTypePublic + default: + return "", errors.New("invalid key purpose") + } + if !paserkType.isAvailableForKey(k) { + return "", InvalidPaserkTypeError{fmt.Sprintf("%T", k), PaserkTypeToString(paserkType)} + } + if paserkType != PaserkTypePublic { + return "", NotImplementedError{fmt.Sprintf("%T", k), PaserkTypeToString(paserkType)} + } + header := KeyVersionToString(k.getVersion()) + "." + PaserkTypeToString(paserkType) + "." + data := base64.RawURLEncoding.EncodeToString(k.ExportBytes()) + return header + data, nil +} + +func ParsePaserkRaw(paserkStr string) (Key, error) { + frags := strings.Split(paserkStr, ".") + if len(frags) != 3 { + return nil, fmt.Errorf("Invalid PASERK token: %s", paserkStr) + } + tokenVersion := KeyVersionFromString(frags[0]) + typ := PaserkTypeFromString(frags[1]) + data, err := base64.RawURLEncoding.DecodeString(frags[2]) + if err != nil { + return nil, errors.Wrap(err, "can't decode data part of pasrk key") + } + + switch tokenVersion { + case KeyVersionV4: + switch typ { + case PaserkTypePublic: + key, err := NewV4AsymmetricPublicKeyFromBytes(data) + if err != nil { + return nil, errors.Wrap(err, "can't construct key from data part of paserk key") + } + return &key, nil + default: + return nil, NotImplementedError{frags[0], frags[1]} + } + default: + return nil, NotImplementedError{frags[0], frags[1]} + } +} diff --git a/test-vectors/PASERK/k4.public.json b/test-vectors/PASERK/k4.public.json new file mode 100644 index 0000000..c060a63 --- /dev/null +++ b/test-vectors/PASERK/k4.public.json @@ -0,0 +1,30 @@ +{ + "name": "PASERK k4.public Test Vectors", + "tests": [ + { + "name": "k4.public-1", + "expect-fail": false, + "key": "0000000000000000000000000000000000000000000000000000000000000000", + "paserk": "k4.public.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + }, + { + "name": "k4.public-2", + "expect-fail": false, + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "paserk": "k4.public.cHFyc3R1dnd4eXp7fH1-f4CBgoOEhYaHiImKi4yNjo8" + }, + { + "name": "k4.public-3", + "expect-fail": false, + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e90", + "paserk": "k4.public.cHFyc3R1dnd4eXp7fH1-f4CBgoOEhYaHiImKi4yNjpA" + }, + { + "name": "k4.public-fail-1", + "expect-fail": true, + "key": "02707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f", + "paserk": null, + "comment": "Implementations MUST NOT accept a PASERK of the wrong version." + } + ] +} \ No newline at end of file diff --git a/v4_keys.go b/v4_keys.go index edd3487..21bfb88 100644 --- a/v4_keys.go +++ b/v4_keys.go @@ -48,6 +48,13 @@ func (k V4AsymmetricPublicKey) ExportBytes() []byte { return k.material } +func (k *V4AsymmetricPublicKey) getVersion() KeyVersion { + return KeyVersionV4 +} +func (k *V4AsymmetricPublicKey) getPurpose() keyPurpose { + return keyPurposePublic +} + // V4AsymmetricSecretKey v4 public private key type V4AsymmetricSecretKey struct { material ed25519.PrivateKey diff --git a/vectors_test.go b/vectors_test.go index 70ee28e..2ea4ba5 100644 --- a/vectors_test.go +++ b/vectors_test.go @@ -27,6 +27,8 @@ type TestVector struct { Footer string ExpectFail bool `json:"expect-fail"` ImplicitAssertation string `json:"implicit-assertion"` + Paserk string + Comment string } func TestV2(t *testing.T) { @@ -299,3 +301,48 @@ func TestV4(t *testing.T) { }) } } + +func TestPaserkV4Public(t *testing.T) { + data, err := os.ReadFile("test-vectors/PASERK/k4.public.json") + require.NoError(t, err) + + var tests TestVectors + err = json.Unmarshal(data, &tests) + require.NoError(t, err) + + for _, test := range tests.Tests { + t.Run(test.Name, func(t *testing.T) { + + k, err := paseto.NewV4AsymmetricPublicKeyFromHex(test.Key) + if test.ExpectFail { + require.Error(t, err) + return + } + require.NoError(t, err) + + token, err := paseto.ExportPaserkRaw(&k) + if test.ExpectFail { + require.Error(t, err) + return + } + require.NoError(t, err) + + require.Equal(t, test.Paserk, token) + }) + } + + for _, test := range tests.Tests { + t.Run(test.Name+"-reverse", func(t *testing.T) { + + k, err := paseto.ParsePaserkRaw(test.Paserk) + if test.ExpectFail { + require.Error(t, err) + return + } + require.NoError(t, err) + + v4key := k.(*paseto.V4AsymmetricPublicKey) + require.Equal(t, test.Key, v4key.ExportHex()) + }) + } +}