diff options
| author | Shulhan <m.shulhan@gmail.com> | 2020-09-09 05:46:36 +0700 |
|---|---|---|
| committer | Shulhan <m.shulhan@gmail.com> | 2020-09-09 08:05:21 +0700 |
| commit | 18ab9aad4c3a99822b9dfdc37a05dae12f4fb05d (patch) | |
| tree | f5b0e438ae06708c1f4a2ba74e2250eed038f0b6 | |
| parent | 9af6d3604d103c55975b70f281b47d11cc9da63d (diff) | |
| download | pakakeh.go-18ab9aad4c3a99822b9dfdc37a05dae12f4fb05d.tar.xz | |
paseto: new package for working with paseto
Package paseto provide the opionated implementation of
Platform-Agnostic SEcurity TOkens (PASETOs) as defined in draft of
RFC 01 [1].
This implementation only support PASETO Protocol v2.
This library focus on how to sign and verify data, everything else is
handled and filled automatically.
[1] https://github.com/paragonie/paseto/blob/master/docs/RFC/draft-paragon-paseto-rfc-01.txt
| -rw-r--r-- | CHANGELOG.adoc | 8 | ||||
| -rw-r--r-- | README.adoc | 3 | ||||
| -rw-r--r-- | _doc/CHANGELOG.html | 81 | ||||
| -rw-r--r-- | _doc/index.html | 4 | ||||
| -rw-r--r-- | lib/paseto/example_local_mode_test.go | 37 | ||||
| -rw-r--r-- | lib/paseto/example_public_mode_test.go | 71 | ||||
| -rw-r--r-- | lib/paseto/json_footer.go | 10 | ||||
| -rw-r--r-- | lib/paseto/json_token.go | 43 | ||||
| -rw-r--r-- | lib/paseto/key.go | 24 | ||||
| -rw-r--r-- | lib/paseto/local_mode.go | 46 | ||||
| -rw-r--r-- | lib/paseto/paseto.go | 318 | ||||
| -rw-r--r-- | lib/paseto/paseto_test.go | 287 | ||||
| -rw-r--r-- | lib/paseto/public_mode.go | 131 |
13 files changed, 1033 insertions, 30 deletions
diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 1300e1f0..73c7b826 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -3,6 +3,14 @@ This library is released each month, usually at the first week of month. +== share v0.20.0 (2020-10-xx) + +=== New features + +* lib/paseto: a simple, ready to use, implementation of Platform-Agnostic + SEcurity TOkens + + == share v0.19.0 (2020-09-08) === Breaking changes diff --git a/README.adoc b/README.adoc index e0d84bc0..fad5c990 100644 --- a/README.adoc +++ b/README.adoc @@ -144,6 +144,9 @@ written in Go. * link:{url-godoc}/lib/parser[*parser*]: Package parser provide a common text parser, using delimiters. +* link:{url-godoc}/lib/paseto[*paseto*]: A simple, ready to use, + implementation of Platform-Agnostic SEcurity TOkens (PASETO). + * link:{url-godoc}/lib/reflect[*reflect*]: Package reflect extends the standard reflect package. diff --git a/_doc/CHANGELOG.html b/_doc/CHANGELOG.html index 1afc2a4c..16778281 100644 --- a/_doc/CHANGELOG.html +++ b/_doc/CHANGELOG.html @@ -235,10 +235,15 @@ dd { <div id="toc" class="toc"> <div id="toctitle">Table of Contents</div> <ul class="sectlevel1"> +<li><a href="#_share_v0_20_0_2020_10_xx">share v0.20.0 (2020-10-xx)</a> +<ul class="sectlevel2"> +<li><a href="#_new_features">New features</a></li> +</ul> +</li> <li><a href="#_share_v0_19_0_2020_09_08">share v0.19.0 (2020-09-08)</a> <ul class="sectlevel2"> <li><a href="#_breaking_changes">Breaking changes</a></li> -<li><a href="#_new_features">New features</a></li> +<li><a href="#_new_features_2">New features</a></li> <li><a href="#_bug_fixes">Bug fixes</a></li> </ul> </li> @@ -260,14 +265,14 @@ dd { <ul class="sectlevel2"> <li><a href="#_breaking_changes_4">Breaking changes</a></li> <li><a href="#_bug_fixes_4">Bug fixes</a></li> -<li><a href="#_new_features_2">New features</a></li> +<li><a href="#_new_features_3">New features</a></li> <li><a href="#_enhancements_3">Enhancements</a></li> </ul> </li> <li><a href="#_share_v0_15_0_2020_05_04">share v0.15.0 (2020-05-04)</a> <ul class="sectlevel2"> <li><a href="#_breaking_changes_5">Breaking changes</a></li> -<li><a href="#_new_features_3">New features</a></li> +<li><a href="#_new_features_4">New features</a></li> <li><a href="#_enhancements_4">Enhancements</a></li> <li><a href="#_bug_fixes_5">Bug fixes</a></li> </ul> @@ -275,7 +280,7 @@ dd { <li><a href="#_share_v0_14_0_2020_04_03">share v0.14.0 (2020-04-03)</a> <ul class="sectlevel2"> <li><a href="#_breaking_changes_6">Breaking changes</a></li> -<li><a href="#_new_features_4">New features</a></li> +<li><a href="#_new_features_5">New features</a></li> <li><a href="#_enhancements_5">Enhancements</a></li> <li><a href="#_bug_fixes_6">Bug fixes</a></li> </ul> @@ -283,7 +288,7 @@ dd { <li><a href="#_share_v0_13_0_2020_03_11">share v0.13.0 (2020-03-11)</a> <ul class="sectlevel2"> <li><a href="#_breaking_changes_7">Breaking changes</a></li> -<li><a href="#_new_features_5">New features</a></li> +<li><a href="#_new_features_6">New features</a></li> <li><a href="#_enhancements_6">Enhancements</a></li> <li><a href="#_bug_fixes_7">Bug Fixes</a></li> </ul> @@ -291,7 +296,7 @@ dd { <li><a href="#_share_v0_12_0_2020_02_13">share v0.12.0 (2020-02-13)</a> <ul class="sectlevel2"> <li><a href="#_breaking_changes_8">Breaking changes</a></li> -<li><a href="#_new_features_6">New features</a></li> +<li><a href="#_new_features_7">New features</a></li> <li><a href="#_enhancements_7">Enhancements</a></li> <li><a href="#_bug_fixes_8">Bug fixes</a></li> </ul> @@ -299,7 +304,7 @@ dd { <li><a href="#_share_v0_11_0_2019_12_26">share v0.11.0 (2019-12-26)</a> <ul class="sectlevel2"> <li><a href="#_breaking_changes_9">Breaking changes</a></li> -<li><a href="#_new_features_7">New features</a></li> +<li><a href="#_new_features_8">New features</a></li> <li><a href="#_enhancements_8">Enhancements</a></li> <li><a href="#_bug_fixes_9">Bug fixes</a></li> </ul> @@ -317,7 +322,7 @@ dd { </li> <li><a href="#_share_v0_10_0_2019_11_05">share v0.10.0 (2019-11-05)</a> <ul class="sectlevel2"> -<li><a href="#_new_features_8">New Features</a></li> +<li><a href="#_new_features_9">New Features</a></li> <li><a href="#_breaking_changes_10">Breaking Changes</a></li> <li><a href="#_enhancements_10">Enhancements</a></li> <li><a href="#_bug_fixes_12">Bug Fixes</a></li> @@ -325,7 +330,7 @@ dd { </li> <li><a href="#_share_v0_9_0_2019_10_08">share v0.9.0 (2019-10-08)</a> <ul class="sectlevel2"> -<li><a href="#_new_features_9">New Features</a></li> +<li><a href="#_new_features_10">New Features</a></li> <li><a href="#_breaking_changes_11">Breaking Changes</a></li> <li><a href="#_bug_fixes_13">Bug Fixes</a></li> <li><a href="#_enhancements_11">Enhancements</a></li> @@ -344,14 +349,14 @@ dd { <li><a href="#_share_v0_8_0_2019_07_09">share v0.8.0 (2019-07-09)</a> <ul class="sectlevel2"> <li><a href="#_breaking_changes_12">Breaking changes</a></li> -<li><a href="#_new_features_10">New Features</a></li> +<li><a href="#_new_features_11">New Features</a></li> <li><a href="#_enhancements_14">Enhancements</a></li> </ul> </li> <li><a href="#_share_v0_7_0_2019_06_14">share v0.7.0 (2019-06-14)</a> <ul class="sectlevel2"> <li><a href="#_breaking_changes_13">Breaking Changes</a></li> -<li><a href="#_new_features_11">New Features</a></li> +<li><a href="#_new_features_12">New Features</a></li> <li><a href="#_enhancements_15">Enhancements</a></li> <li><a href="#_bug_fixes_14">Bug Fixes</a></li> </ul> @@ -360,20 +365,20 @@ dd { <li><a href="#_share_v0_6_0_2019_05_07">share v0.6.0 (2019-05-07)</a> <ul class="sectlevel2"> <li><a href="#_breaking_changes_14">Breaking Changes</a></li> -<li><a href="#_new_features_12">New Features</a></li> +<li><a href="#_new_features_13">New Features</a></li> <li><a href="#_bug_fixes_15">Bug Fixes</a></li> <li><a href="#_documentation">Documentation</a></li> </ul> </li> <li><a href="#_share_v0_5_0_2019_04_02">share v0.5.0 (2019-04-02)</a> <ul class="sectlevel2"> -<li><a href="#_new_features_13">New Features</a></li> +<li><a href="#_new_features_14">New Features</a></li> <li><a href="#_enhancements_16">Enhancements</a></li> </ul> </li> <li><a href="#_share_v0_4_0_2019_03_01">share v0.4.0 (2019-03-01)</a> <ul class="sectlevel2"> -<li><a href="#_new_features_14">New Features</a></li> +<li><a href="#_new_features_15">New Features</a></li> <li><a href="#_enhancements_17">Enhancements</a></li> <li><a href="#_fixes">Fixes</a></li> </ul> @@ -387,7 +392,7 @@ dd { </li> <li><a href="#_share_v0_2_0_2019_01_02">share v0.2.0 (2019-01-02)</a> <ul class="sectlevel2"> -<li><a href="#_new_features_15">New Features</a></li> +<li><a href="#_new_features_16">New Features</a></li> <li><a href="#_enhancements_19">Enhancements</a></li> </ul> </li> @@ -402,6 +407,22 @@ dd { </div> </div> <div class="sect1"> +<h2 id="_share_v0_20_0_2020_10_xx">share v0.20.0 (2020-10-xx)</h2> +<div class="sectionbody"> +<div class="sect2"> +<h3 id="_new_features">New features</h3> +<div class="ulist"> +<ul> +<li> +<p>lib/paseto: a simple, ready to use, implementation of Platform-Agnostic + SEcurity TOkens</p> +</li> +</ul> +</div> +</div> +</div> +</div> +<div class="sect1"> <h2 id="_share_v0_19_0_2020_09_08">share v0.19.0 (2020-09-08)</h2> <div class="sectionbody"> <div class="sect2"> @@ -467,7 +488,7 @@ Messages.</pre> </div> </div> <div class="sect2"> -<h3 id="_new_features">New features</h3> +<h3 id="_new_features_2">New features</h3> <div class="ulist"> <ul> <li> @@ -770,7 +791,7 @@ become unneeded, so we remove them.</pre> </div> </div> <div class="sect2"> -<h3 id="_new_features_2">New features</h3> +<h3 id="_new_features_3">New features</h3> <div class="ulist"> <ul> <li> @@ -929,7 +950,7 @@ fis, err := root.Readdir(0)</pre> </div> </div> <div class="sect2"> -<h3 id="_new_features_3">New features</h3> +<h3 id="_new_features_4">New features</h3> <div class="ulist"> <ul> <li> @@ -1025,7 +1046,7 @@ will be handled automatically based on value on field Method.</pre> </div> </div> <div class="sect2"> -<h3 id="_new_features_4">New features</h3> +<h3 id="_new_features_5">New features</h3> <div class="ulist"> <ul> <li> @@ -1140,7 +1161,7 @@ word.</pre> </div> </div> <div class="sect2"> -<h3 id="_new_features_5">New features</h3> +<h3 id="_new_features_6">New features</h3> <div class="ulist"> <ul> <li> @@ -1212,7 +1233,7 @@ word.</pre> </div> </div> <div class="sect2"> -<h3 id="_new_features_6">New features</h3> +<h3 id="_new_features_7">New features</h3> <div class="ulist"> <ul> <li> @@ -1263,7 +1284,7 @@ word.</pre> </div> </div> <div class="sect2"> -<h3 id="_new_features_7">New features</h3> +<h3 id="_new_features_8">New features</h3> <div class="ulist"> <ul> <li> @@ -1425,7 +1446,7 @@ word.</pre> <h2 id="_share_v0_10_0_2019_11_05">share v0.10.0 (2019-11-05)</h2> <div class="sectionbody"> <div class="sect2"> -<h3 id="_new_features_8">New Features</h3> +<h3 id="_new_features_9">New Features</h3> <div class="ulist"> <ul> <li> @@ -1506,7 +1527,7 @@ word.</pre> <h2 id="_share_v0_9_0_2019_10_08">share v0.9.0 (2019-10-08)</h2> <div class="sectionbody"> <div class="sect2"> -<h3 id="_new_features_9">New Features</h3> +<h3 id="_new_features_10">New Features</h3> <div class="ulist"> <ul> <li> @@ -1717,7 +1738,7 @@ file, we want that file to be excluded from .go static source.</p> </div> </div> <div class="sect2"> -<h3 id="_new_features_10">New Features</h3> +<h3 id="_new_features_11">New Features</h3> <div class="ulist"> <ul> <li> @@ -1765,7 +1786,7 @@ simple API.</p> </div> </div> <div class="sect2"> -<h3 id="_new_features_11">New Features</h3> +<h3 id="_new_features_12">New Features</h3> <div class="ulist"> <ul> <li> @@ -1964,7 +1985,7 @@ removing the server handler.</p> </div> </div> <div class="sect2"> -<h3 id="_new_features_12">New Features</h3> +<h3 id="_new_features_13">New Features</h3> <div class="ulist"> <ul> <li> @@ -2100,7 +2121,7 @@ server and client API to make it easy and extensible. The websocket is now 100% pass the autobahn testsuite (minus compression feature).</p> </div> <div class="sect2"> -<h3 id="_new_features_13">New Features</h3> +<h3 id="_new_features_14">New Features</h3> <div class="ulist"> <ul> <li> @@ -2167,7 +2188,7 @@ server and client API to make it easy and extensible. The websocket is now <h2 id="_share_v0_4_0_2019_03_01">share v0.4.0 (2019-03-01)</h2> <div class="sectionbody"> <div class="sect2"> -<h3 id="_new_features_14">New Features</h3> +<h3 id="_new_features_15">New Features</h3> <div class="ulist"> <ul> <li> @@ -2413,7 +2434,7 @@ server and client API to make it easy and extensible. The websocket is now <h2 id="_share_v0_2_0_2019_01_02">share v0.2.0 (2019-01-02)</h2> <div class="sectionbody"> <div class="sect2"> -<h3 id="_new_features_15">New Features</h3> +<h3 id="_new_features_16">New Features</h3> <div class="ulist"> <ul> <li> diff --git a/_doc/index.html b/_doc/index.html index d1e926a2..3587dcb2 100644 --- a/_doc/index.html +++ b/_doc/index.html @@ -459,6 +459,10 @@ written in Go.</p> parser, using delimiters.</p> </li> <li> +<p><a href="https://pkg.go.dev/github.com/shuLhan/share/lib/paseto"><strong>paseto</strong></a>: A simple, ready to use, + implementation of Platform-Agnostic SEcurity TOkens (PASETO).</p> +</li> +<li> <p><a href="https://pkg.go.dev/github.com/shuLhan/share/lib/reflect"><strong>reflect</strong></a>: Package reflect extends the standard reflect package.</p> </li> diff --git a/lib/paseto/example_local_mode_test.go b/lib/paseto/example_local_mode_test.go new file mode 100644 index 00000000..5782037e --- /dev/null +++ b/lib/paseto/example_local_mode_test.go @@ -0,0 +1,37 @@ +package paseto + +import ( + "encoding/hex" + "fmt" + "log" +) + +func ExampleLocalMode() { + // + // In local mode, we create sender and receiver using the same key. + // + key, _ := hex.DecodeString("707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f") + sender, _ := NewLocalMode(key) + receiver, _ := NewLocalMode(key) + + token, err := sender.Pack([]byte("Hello receiver"), []byte(">>> footer")) + if err != nil { + log.Fatal(err) + } + + // Sender then send the encrypted token to receiver + // ... + + // Receiver unpack the token from sender to get the plain text and + // footer. + plain, footer, err := receiver.Unpack(token) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Receive data from sender: %s\n", plain) + fmt.Printf("Receive footer from sender: %s\n", footer) + // Output: + // Receive data from sender: Hello receiver + // Receive footer from sender: >>> footer +} diff --git a/lib/paseto/example_public_mode_test.go b/lib/paseto/example_public_mode_test.go new file mode 100644 index 00000000..a5004a2e --- /dev/null +++ b/lib/paseto/example_public_mode_test.go @@ -0,0 +1,71 @@ +// Copyright 2020, Shulhan <ms@kilabit.info>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package paseto + +import ( + "crypto/ed25519" + "encoding/hex" + "fmt" + "log" +) + +func ExamplePublicMode() { + senderSK, _ := hex.DecodeString("e9ae9c7eae2fce6fd6727b5ca8df0fbc0aa60a5ffb354d4fdee1729e4e1463688d2160a4dc71a9a697d6ad6424da3f9dd18a259cdd51b0ae2b521e998b82d36e") + senderPK, _ := hex.DecodeString("8d2160a4dc71a9a697d6ad6424da3f9dd18a259cdd51b0ae2b521e998b82d36e") + senderKey := Key{ + id: "sender", + private: ed25519.PrivateKey(senderSK), + public: ed25519.PublicKey(senderPK), + } + + receiverSK, _ := hex.DecodeString("4983da648bff1fd3e1892df9c56370215aa640829a5cab02d6616b115fa0bc5707c22e74ab9b181f8d87bdf03cf88476ec4c35e5517e173f236592f6695d59f5") + receiverPK, _ := hex.DecodeString("07c22e74ab9b181f8d87bdf03cf88476ec4c35e5517e173f236592f6695d59f5") + receiverKey := Key{ + id: "receiver", + private: ed25519.PrivateKey(receiverSK), + public: ed25519.PublicKey(receiverPK), + } + + // + // In the sender part, we register the sender key and the public key + // of receiver in the list of peers. + // + senderPeers := map[string]ed25519.PublicKey{ + receiverKey.id: receiverKey.public, + } + sender := NewPublicMode(senderKey, senderPeers) + + addFooter := map[string]interface{}{ + "FOOTER": "HERE", + } + token, err := sender.Pack([]byte("hello receiver"), addFooter) + if err != nil { + log.Fatal(err) + } + + // token generated by sender and send to receiver + // ... + + // + // In the receiver part, we register the receiver key and the public key + // of sender in the list of peers. + // + receiverPeers := map[string]ed25519.PublicKey{ + senderKey.id: senderKey.public, + } + receiver := NewPublicMode(receiverKey, receiverPeers) + + // receiver receive the token from sender and unpack it ... + gotData, gotFooter, err := receiver.Unpack(token) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Received data: %s\n", gotData) + fmt.Printf("Received footer: %+v\n", gotFooter) + // Output: + // Received data: hello receiver + // Received footer: map[FOOTER:HERE] +} diff --git a/lib/paseto/json_footer.go b/lib/paseto/json_footer.go new file mode 100644 index 00000000..6a720051 --- /dev/null +++ b/lib/paseto/json_footer.go @@ -0,0 +1,10 @@ +// Copyright 2020, Shulhan <ms@kilabit.info>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package paseto + +type JSONFooter struct { + KID string `json:"kid"` + Data map[string]interface{} `json:"data"` +} diff --git a/lib/paseto/json_token.go b/lib/paseto/json_token.go new file mode 100644 index 00000000..272a1ecf --- /dev/null +++ b/lib/paseto/json_token.go @@ -0,0 +1,43 @@ +// Copyright 2020, Shulhan <ms@kilabit.info>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package paseto + +import ( + "fmt" + "time" +) + +const ( + dateTimeLayout = "2006-01-02T15:04:05-07:00" +) + +type JSONToken struct { + Issuer string `json:"iss,omitempty"` + Subject string `json:"sub,omitempty"` + Audience string `json:"aud,omitempty"` + ExpiredAt *time.Time `json:"exp,omitempty"` + NotBefore *time.Time `json:"nbf,omitempty"` + IssuedAt *time.Time `json:"iat,omitempty"` + TokenID string `json:"jti,omitempty"` + Data string `json:"data"` +} + +// +// Validate the ExpiredAt and NotBefore time fields. +// +func (jtoken *JSONToken) Validate() (err error) { + now := time.Now() + if jtoken.ExpiredAt != nil { + if now.After(*jtoken.ExpiredAt) { + return fmt.Errorf("token is expired") + } + } + if jtoken.NotBefore != nil { + if now.Before(*jtoken.NotBefore) { + return fmt.Errorf("token is too early") + } + } + return nil +} diff --git a/lib/paseto/key.go b/lib/paseto/key.go new file mode 100644 index 00000000..48a5eaf4 --- /dev/null +++ b/lib/paseto/key.go @@ -0,0 +1,24 @@ +// Copyright 2020, Shulhan <ms@kilabit.info>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package paseto + +import "crypto/ed25519" + +type Key struct { + id string + private ed25519.PrivateKey + public ed25519.PublicKey +} + +// +// NewKey create new Key from hex encoded strings. +// +func NewKey(id string, private ed25519.PrivateKey, public ed25519.PublicKey) Key { + return Key{ + id: id, + private: private, + public: public, + } +} diff --git a/lib/paseto/local_mode.go b/lib/paseto/local_mode.go new file mode 100644 index 00000000..2853c3df --- /dev/null +++ b/lib/paseto/local_mode.go @@ -0,0 +1,46 @@ +// Copyright 2020, Shulhan <ms@kilabit.info>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package paseto + +import ( + "crypto/cipher" + + "golang.org/x/crypto/chacha20poly1305" +) + +// +// LocalMode implement the PASETO encrypt and decrypt using shared key. +// +type LocalMode struct { + aead cipher.AEAD +} + +// +// NewLocalMode create and initialize new LocalMode using shared key. +// +func NewLocalMode(key []byte) (local *LocalMode, err error) { + local = &LocalMode{} + + local.aead, err = chacha20poly1305.NewX(key) + if err != nil { + return nil, err + } + + return local, nil +} + +// +// Pack encrypt the data and generate token with optional footer. +// +func (l *LocalMode) Pack(data, footer []byte) (token string, err error) { + return Encrypt(l.aead, data, footer) +} + +// +// Unpack decrypt the token and return the plain data and optional footer. +// +func (l *LocalMode) Unpack(token string) (data, footer []byte, err error) { + return Decrypt(l.aead, token) +} diff --git a/lib/paseto/paseto.go b/lib/paseto/paseto.go new file mode 100644 index 00000000..3025d71a --- /dev/null +++ b/lib/paseto/paseto.go @@ -0,0 +1,318 @@ +// Copyright 2020, Shulhan <ms@kilabit.info>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +// Package paseto provide a simple, ready to use, implementation of +// Platform-Agnostic SEcurity TOkens (PASETOs) v2 as defined in draft of RFC +// 01 [1]. +// +// Limitation +// +// This implementation only support PASETO Protocol v2. +// +// Local mode +// +// The local mode use crypto/rand package to generate random nonce and hashed +// with blake2b. +// +// Public mode +// +// The public mode focus on signing and verifing data, everything else is +// handled and filled automatically. +// +// For example, when generating token for signing, the user data is stored +// using key "data" inside the JSON token, encoded using base64. +// The Issuer will be set to the Key's ID, the expiration date is set to +// current time plus TTL. +// The footer will always generated using JSONFooter with KID (Key-ID) set to +// the Key's ID. +// Additional footer data can be added on the Data field. +// +// When verifying token, the key ID is read from footer and verified using one +// of the public key registered previously. +// +// Overall, the following JSONToken and JSONFooter is generated for each +// token, +// +// JSONToken:{ +// "iss": <Key.ID>, +// "exp": <time.Now() + TTL>, +// "iat": <time.Now()>, +// "data": <base64.StdEncoding.EncodeToString(userData)>, +// } +// JSONFooter:{ +// "kid": <Key.ID>, +// "data": {} +// } +// +// References +// +// [1] https://github.com/paragonie/paseto/blob/master/docs/RFC/draft-paragon-paseto-rfc-01.txt +// +package paseto + +import ( + "bytes" + "crypto/cipher" + "crypto/ed25519" + "crypto/rand" + "encoding/base64" + "errors" + "fmt" + "strings" + + "golang.org/x/crypto/blake2b" +) + +const ( + randNonceSize = 24 +) + +var ( + headerModePublic = []byte("v2.public.") + headerModeLocal = []byte("v2.local.") +) + +// +// Encrypt given the shared key, encrypt the plain message and generate the +// "local" token with optional footer. +// +func Encrypt(aead cipher.AEAD, plain, footer []byte) (token string, err error) { + nonce := make([]byte, randNonceSize) + _, err = rand.Read(nonce) + if err != nil { + return "", err + } + + return encrypt(aead, nonce, plain, footer) +} + +func encrypt(aead cipher.AEAD, nonce, plain, footer []byte) (token string, err error) { + b2b, err := blake2b.New(randNonceSize, nonce) + if err != nil { + return "", err + } + + _, err = b2b.Write(plain) + if err != nil { + return "", err + } + + nonce = b2b.Sum(nil) + + pieces := [][]byte{headerModeLocal, nonce, footer} + + m2, err := pae(pieces) + if err != nil { + return "", err + } + + cipher := aead.Seal(nil, nonce, plain, m2) + + var buf bytes.Buffer + + _, err = buf.Write(headerModeLocal) + if err != nil { + return "", err + } + + sc := make([]byte, 0, len(nonce)+len(cipher)) + sc = append(sc, nonce...) + sc = append(sc, cipher...) + + n := base64.RawURLEncoding.EncodedLen(len(sc)) + dst := make([]byte, n) + base64.RawURLEncoding.Encode(dst, sc) + _, err = buf.Write(dst) + if err != nil { + return "", nil + } + + if len(footer) > 0 { + buf.WriteByte('.') + + n = base64.RawURLEncoding.EncodedLen(len(footer)) + dst = make([]byte, n) + base64.RawURLEncoding.Encode(dst, footer) + _, err = buf.Write(dst) + if err != nil { + return "", nil + } + } + + return buf.String(), nil +} + +// +// Decrypt given a shared key and encrypted token, decrypt the token to get +// the message. +// +func Decrypt(aead cipher.AEAD, token string) (plain, footer []byte, err error) { + pieces := strings.Split(token, ".") + if len(pieces) < 3 || len(pieces) > 4 { + return nil, nil, errors.New("invalid token format") + } + if pieces[0] != "v2" { + return nil, nil, fmt.Errorf("unsupported protocol version " + pieces[0]) + } + if pieces[1] != "local" { + return nil, nil, fmt.Errorf("expecting local mode, got " + pieces[1]) + } + + if len(pieces) == 4 { + footer, err = base64.RawURLEncoding.DecodeString(pieces[3]) + if err != nil { + return nil, nil, err + } + } + + src, err := base64.RawURLEncoding.DecodeString(pieces[2]) + if err != nil { + return nil, nil, err + } + + nonce := src[:randNonceSize] + cipher := src[randNonceSize:] + + if len(cipher) < aead.NonceSize() { + return nil, nil, errors.New("ciphertext too short") + } + + m2, err := pae([][]byte{headerModeLocal, nonce, footer}) + if err != nil { + return nil, nil, err + } + + plain, err = aead.Open(nil, nonce, cipher, m2) + if err != nil { + return nil, nil, err + } + + return plain, footer, nil +} + +// +// Sign given an Ed25519 secret key "sk", a message "m", and optional footer +// "f" (which defaults to empty string); sign the message "m" and generate the +// public token. +// +func Sign(sk ed25519.PrivateKey, m, f []byte) (token string, err error) { + pieces := [][]byte{headerModePublic, m, f} + + m2, err := pae(pieces) + if err != nil { + return "", err + } + + sig := ed25519.Sign(sk, m2) + + var buf bytes.Buffer + + _, err = buf.Write(headerModePublic) + if err != nil { + return "", err + } + + sm := make([]byte, 0, len(m)+len(sig)) + sm = append(sm, m...) + sm = append(sm, sig...) + + n := base64.RawURLEncoding.EncodedLen(len(sm)) + dst := make([]byte, n) + base64.RawURLEncoding.Encode(dst, sm) + + _, err = buf.Write(dst) + if err != nil { + return "", err + } + + if len(f) > 0 { + _ = buf.WriteByte('.') + + n = base64.RawURLEncoding.EncodedLen(len(f)) + dst = make([]byte, n) + base64.RawURLEncoding.Encode(dst, f) + + _, err = buf.Write(dst) + if err != nil { + return "", err + } + } + + return buf.String(), nil +} + +// +// Verify given a public key "pk", a signed message "sm" (that has been +// decoded from base64), and optional footer "f" (also that has been decoded +// from base64 string); verify that the signature is valid for the message. +// +func Verify(pk ed25519.PublicKey, sm, f []byte) (msg []byte, err error) { + if len(sm) <= 64 { + return nil, fmt.Errorf("invalid signed message length") + } + + msg = sm[:len(sm)-64] + sig := sm[len(sm)-64:] + pieces := [][]byte{headerModePublic, msg, f} + + msg2, err := pae(pieces) + if err != nil { + return nil, err + } + + if !ed25519.Verify(pk, msg2, sig) { + return nil, fmt.Errorf("invalid message signature") + } + + return msg, nil +} + +func pae(pieces [][]byte) (b []byte, err error) { + var buf bytes.Buffer + + b, err = le64(int64(len(pieces))) + if err != nil { + return nil, err + } + + _, err = buf.Write(b) + if err != nil { + return nil, err + } + + for x := 0; x < len(pieces); x++ { + b, err = le64(int64(len(pieces[x]))) + if err != nil { + return nil, err + } + + _, err = buf.Write(b) + if err != nil { + return nil, err + } + + _, err = buf.Write(pieces[x]) + if err != nil { + return nil, err + } + } + return buf.Bytes(), nil +} + +func le64(n int64) (out []byte, err error) { + var buf bytes.Buffer + + for x := 0; x < 8; x++ { + if x == 7 { + n &= 127 + } + _, err = buf.WriteRune(rune(n & 255)) + if err != nil { + return out, err + } + n = n >> 8 + } + return buf.Bytes(), nil +} diff --git a/lib/paseto/paseto_test.go b/lib/paseto/paseto_test.go new file mode 100644 index 00000000..4d52121f --- /dev/null +++ b/lib/paseto/paseto_test.go @@ -0,0 +1,287 @@ +// Copyright 2020, Shulhan <ms@kilabit.info>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package paseto + +import ( + "encoding/base64" + "encoding/hex" + "strings" + "testing" + + "github.com/shuLhan/share/lib/test" + "golang.org/x/crypto/chacha20poly1305" +) + +func TestPae(t *testing.T) { + cases := []struct { + pieces [][]byte + exp []byte + }{{ + exp: []byte("\x00\x00\x00\x00\x00\x00\x00\x00"), + }, { + pieces: [][]byte{[]byte{}}, + exp: []byte("\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"), + }, { + pieces: [][]byte{[]byte("test")}, + exp: []byte("\x01\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00test"), + }} + + for _, c := range cases { + got, err := pae(c.pieces) + if err != nil { + t.Fatal(err) + } + + test.Assert(t, "pae", c.exp, got, true) + } +} + +func TestEncrypt(t *testing.T) { + hexKey := "70717273" + "74757677" + "78797a7b" + "7c7d7e7f" + + "80818283" + "84858687" + "88898a8b" + "8c8d8e8f" + + key, err := hex.DecodeString(hexKey) + if err != nil { + t.Fatal(err) + } + + aead, err := chacha20poly1305.NewX(key) + if err != nil { + t.Fatal(err) + } + + cases := []struct { + desc string + msg []byte + nonce string + footer []byte + exp string + }{{ + desc: "Encrypt with zero nonce, without footer", + msg: []byte(`{"data":"this is a signed message","exp":"2019-01-01T00:00:00+00:00"}`), + nonce: "00000000" + "00000000" + "00000000" + "00000000" + + "00000000" + "00000000", + exp: "v2.local.97TTOvgwIxNGvV80XKiGZg_kD3tsXM_-qB4dZGHOeN1cTkgQ4Pn" + + "W8888l802W8d9AvEGnoNBY3BnqHORy8a5cC8aKpbA0En8XELw2yDk2f1sVOD" + + "yfnDbi6rEGMY3pSfCbLWMM2oHJxvlEl2XbQ", + }, { + desc: "Encrypt with zero nonce, without footer (2)", + msg: []byte(`{"data":"this is a secret message","exp":"2019-01-01T00:00:00+00:00"}`), + nonce: "00000000" + "00000000" + "00000000" + "00000000" + + "00000000" + "00000000", + exp: "v2.local.CH50H-HM5tzdK4kOmQ8KbIvrzJfjYUGuu5Vy9ARSFHy9owVDMYg" + + "3-8rwtJZQjN9ABHb2njzFkvpr5cOYuRyt7CRXnHt42L5yZ7siD-4l-FoNsC7" + + "J2OlvLlIwlG06mzQVunrFNb7Z3_CHM0PK5w", + }, { + desc: "Encrypt with nonce, without footer", + msg: []byte(`{"data":"this is a signed message","exp":"2019-01-01T00:00:00+00:00"}`), + nonce: "45742c97" + "6d684ff8" + "4ebdc0de" + "59809a97" + + "cda2f64c" + "84fda19b", + exp: "v2.local.5K4SCXNhItIhyNuVIZcwrdtaDKiyF81-eWHScuE0idiVqCo72bb" + + "jo07W05mqQkhLZdVbxEa5I_u5sgVk1QLkcWEcOSlLHwNpCkvmGGlbCdNExn6" + + "Qclw3qTKIIl5-O5xRBN076fSDPo5xUCPpBA", + }, { + desc: "Encrypt with nonce, with footer", + msg: []byte(`{"data":"this is a signed message","exp":"2019-01-01T00:00:00+00:00"}`), + nonce: "45742c97" + "6d684ff8" + "4ebdc0de" + "59809a97" + + "cda2f64c" + "84fda19b", + footer: []byte(`{"kid":"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN"}`), + exp: "v2.local.5K4SCXNhItIhyNuVIZcwrdtaDKiyF81-eWHScuE0idiVqCo72bb" + + "jo07W05mqQkhLZdVbxEa5I_u5sgVk1QLkcWEcOSlLHwNpCkvmGGlbCdNExn6" + + "Qclw3qTKIIl5-zSLIrxZqOLwcFLYbVK1SrQ.eyJraWQiOiJ6VmhNaVBCUDlm" + + "UmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", + }, { + desc: "Encrypt with nonce, with footer (2)", + msg: []byte(`{"data":"this is a secret message","exp":"2019-01-01T00:00:00+00:00"}`), + nonce: "45742c97" + "6d684ff8" + "4ebdc0de" + "59809a97" + + "cda2f64c" + "84fda19b", + footer: []byte(`{"kid":"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN"}`), + exp: "v2.local.pvFdDeNtXxknVPsbBCZF6MGedVhPm40SneExdClOxa9HNR8wFv7" + + "cu1cB0B4WxDdT6oUc2toyLR6jA6sc-EUM5ll1EkeY47yYk6q8m1RCpqTIzUr" + + "Iu3B6h232h62DnMXKdHn_Smp6L_NfaEnZ-A.eyJraWQiOiJ6VmhNaVBCUDlm" + + "UmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", + }} + + for _, c := range cases { + nonce, err := hex.DecodeString(c.nonce) + if err != nil { + t.Fatal(err) + } + + got, err := encrypt(aead, nonce, c.msg, c.footer) + if err != nil { + t.Fatal(err) + } + + test.Assert(t, c.desc, c.exp, got, true) + } +} + +func TestDecrypt(t *testing.T) { + hexKey := "70717273" + "74757677" + "78797a7b" + "7c7d7e7f" + + "80818283" + "84858687" + "88898a8b" + "8c8d8e8f" + + key, err := hex.DecodeString(hexKey) + if err != nil { + t.Fatal(err) + } + + aead, err := chacha20poly1305.NewX(key) + if err != nil { + t.Fatal(err) + } + + cases := []struct { + desc string + token string + exp []byte + expFooter []byte + }{{ + desc: "Decrypt without nonce and footer", + token: "v2.local.97TTOvgwIxNGvV80XKiGZg_kD3tsXM_-qB4dZGHOeN1cTkgQ4Pn" + + "W8888l802W8d9AvEGnoNBY3BnqHORy8a5cC8aKpbA0En8XELw2yDk2f1sVOD" + + "yfnDbi6rEGMY3pSfCbLWMM2oHJxvlEl2XbQ", + exp: []byte(`{"data":"this is a signed message","exp":"2019-01-01T00:00:00+00:00"}`), + }, { + desc: "Decrypt without nonce and footer (2)", + token: "v2.local.CH50H-HM5tzdK4kOmQ8KbIvrzJfjYUGuu5Vy9ARSFHy9owVDMYg" + + "3-8rwtJZQjN9ABHb2njzFkvpr5cOYuRyt7CRXnHt42L5yZ7siD-4l-FoNsC7" + + "J2OlvLlIwlG06mzQVunrFNb7Z3_CHM0PK5w", + exp: []byte(`{"data":"this is a secret message","exp":"2019-01-01T00:00:00+00:00"}`), + }, { + desc: "Decrypt with nonce, without footer", + token: "v2.local.5K4SCXNhItIhyNuVIZcwrdtaDKiyF81-eWHScuE0idiVqCo72bb" + + "jo07W05mqQkhLZdVbxEa5I_u5sgVk1QLkcWEcOSlLHwNpCkvmGGlbCdNExn6" + + "Qclw3qTKIIl5-O5xRBN076fSDPo5xUCPpBA", + exp: []byte(`{"data":"this is a signed message","exp":"2019-01-01T00:00:00+00:00"}`), + }, { + desc: "Decrypt with nonce, with footer", + token: "v2.local.5K4SCXNhItIhyNuVIZcwrdtaDKiyF81-eWHScuE0idiVqCo72bb" + + "jo07W05mqQkhLZdVbxEa5I_u5sgVk1QLkcWEcOSlLHwNpCkvmGGlbCdNExn6" + + "Qclw3qTKIIl5-zSLIrxZqOLwcFLYbVK1SrQ.eyJraWQiOiJ6VmhNaVBCUDlm" + + "UmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", + exp: []byte(`{"data":"this is a signed message","exp":"2019-01-01T00:00:00+00:00"}`), + expFooter: []byte(`{"kid":"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN"}`), + }, { + desc: "Decrypt with nonce, with footer (2)", + token: "v2.local.pvFdDeNtXxknVPsbBCZF6MGedVhPm40SneExdClOxa9HNR8wFv7" + + "cu1cB0B4WxDdT6oUc2toyLR6jA6sc-EUM5ll1EkeY47yYk6q8m1RCpqTIzUr" + + "Iu3B6h232h62DnMXKdHn_Smp6L_NfaEnZ-A.eyJraWQiOiJ6VmhNaVBCUDlm" + + "UmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", + exp: []byte(`{"data":"this is a secret message","exp":"2019-01-01T00:00:00+00:00"}`), + expFooter: []byte(`{"kid":"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN"}`), + }} + + for _, c := range cases { + got, gotFooter, err := Decrypt(aead, c.token) + if err != nil { + t.Fatal(err) + } + + test.Assert(t, c.desc, c.exp, got, true) + test.Assert(t, c.desc, c.expFooter, gotFooter, true) + } +} + +func TestSign(t *testing.T) { + hexPrivate := "b4cbfb43" + "df4ce210" + "727d953e" + "4a713307" + + "fa19bb7d" + "9f850414" + "38d9e11b" + "942a3774" + + "1eb9dbbb" + "bc047c03" + "fd70604e" + "0071f098" + + "7e16b28b" + "757225c1" + "1f00415d" + "0e20b1a2" + + sk, err := hex.DecodeString(hexPrivate) + if err != nil { + t.Fatal() + } + + m := []byte(`{"data":"this is a signed message","exp":"2019-01-01T00:00:00+00:00"}`) + + cases := []struct { + desc string + m []byte + f []byte + exp string + }{{ + desc: "Sign", + m: m, + exp: "v2.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIi" + + "wiZXhwIjoiMjAxOS0wMS0wMVQwMDowMDowMCswMDowMCJ9HQr8URrGnt" + + "Tu7Dz9J2IF23d1M7-9lH9xiqdGyJNvzp4angPW5Esc7C5huy_M8I8_Dj" + + "JK2ZXC2SUYuOFM-Q_5Cw", + }, { + desc: "Sign with footer", + m: m, + f: []byte(`{"kid":"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN"}`), + exp: "v2.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIi" + + "wiZXhwIjoiMjAxOS0wMS0wMVQwMDowMDowMCswMDowMCJ9flsZsx_gYC" + + "R0N_Ec2QxJFFpvQAs7h9HtKwbVK2n1MJ3Rz-hwe8KUqjnd8FAnIJZ601" + + "tp7lGkguU63oGbomhoBw.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q" + + "3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", + }} + + for _, c := range cases { + got, err := Sign(sk, c.m, c.f) + if err != nil { + t.Fatal(err) + } + + test.Assert(t, c.desc, c.exp, got, true) + } +} + +func TestVerify(t *testing.T) { + hexPublic := "1eb9dbbb" + "bc047c03" + "fd70604e" + "0071f098" + + "7e16b28b" + "757225c1" + "1f00415d" + "0e20b1a2" + + public, err := hex.DecodeString(hexPublic) + if err != nil { + t.Fatal() + } + + cases := []struct { + desc string + token string + exp string + }{{ + desc: "Verify", + token: "v2.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIi" + + "wiZXhwIjoiMjAxOS0wMS0wMVQwMDowMDowMCswMDowMCJ9HQr8URrGnt" + + "Tu7Dz9J2IF23d1M7-9lH9xiqdGyJNvzp4angPW5Esc7C5huy_M8I8_Dj" + + "JK2ZXC2SUYuOFM-Q_5Cw", + exp: `{"data":"this is a signed message","exp":"2019-01-01T00:00:00+00:00"}`, + }, { + desc: "Verify with footer", + token: "v2.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIi" + + "wiZXhwIjoiMjAxOS0wMS0wMVQwMDowMDowMCswMDowMCJ9flsZsx_gYC" + + "R0N_Ec2QxJFFpvQAs7h9HtKwbVK2n1MJ3Rz-hwe8KUqjnd8FAnIJZ601" + + "tp7lGkguU63oGbomhoBw.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q" + + "3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", + exp: `{"data":"this is a signed message","exp":"2019-01-01T00:00:00+00:00"}`, + }} + + for _, c := range cases { + var footer []byte + + pieces := strings.Split(c.token, ".") + + sm, err := base64.RawURLEncoding.DecodeString(pieces[2]) + if err != nil { + t.Fatal(err) + } + if len(pieces) == 4 { + footer, err = base64.RawURLEncoding.DecodeString(pieces[3]) + if err != nil { + t.Fatal(err) + } + } + + got, err := Verify(public, sm, footer) + if err != nil { + t.Fatal(err) + } + + test.Assert(t, c.desc, c.exp, string(got), true) + } +} diff --git a/lib/paseto/public_mode.go b/lib/paseto/public_mode.go new file mode 100644 index 00000000..41c4ef34 --- /dev/null +++ b/lib/paseto/public_mode.go @@ -0,0 +1,131 @@ +// Copyright 2020, Shulhan <ms@kilabit.info>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package paseto + +import ( + "crypto/ed25519" + "encoding/base64" + "encoding/json" + "fmt" + "strings" + "time" +) + +var DefaultTTL = 60 * time.Second + +// +// PublicMode implement the PASETO public mode to signing and verifying data +// using private key and one or more shared public keys. +// +type PublicMode struct { + our Key + peers map[string]ed25519.PublicKey +} + +// +// NewPublicMode create new PublicMode with our private key for signing +// outgoing token and list of peer public keys for verifying the incoming +// token. +// +func NewPublicMode(our Key, peers map[string]ed25519.PublicKey) (auth *PublicMode) { + auth = &PublicMode{ + our: our, + peers: peers, + } + + return auth +} + +// +// Pack the data into token. +// +func (auth *PublicMode) Pack(data []byte, addFooter map[string]interface{}) ( + token string, err error, +) { + now := time.Now() + expiredAt := now.Add(DefaultTTL) + jsonToken := JSONToken{ + Issuer: auth.our.id, + IssuedAt: &now, + ExpiredAt: &expiredAt, + Data: base64.StdEncoding.EncodeToString(data), + } + + msg, err := json.Marshal(&jsonToken) + if err != nil { + return "", err + } + + jsonFooter := JSONFooter{ + KID: auth.our.id, + Data: addFooter, + } + + footer, err := json.Marshal(&jsonFooter) + if err != nil { + return "", err + } + + return Sign(auth.our.private, msg, footer) +} + +// +// Unpack the token to get the JSONToken and the data. +// +func (auth *PublicMode) Unpack(token string) (data []byte, addFooter map[string]interface{}, err error) { + pieces := strings.Split(token, ".") + if len(pieces) != 4 { + return nil, nil, fmt.Errorf("invalid token format") + } + if pieces[0] != "v2" { + return nil, nil, fmt.Errorf("unsupported protocol version " + pieces[0]) + } + if pieces[1] != "public" { + return nil, nil, fmt.Errorf("expecting public mode, got " + pieces[1]) + } + + footer, err := base64.RawURLEncoding.DecodeString(pieces[3]) + if err != nil { + return nil, nil, err + } + + jsonFooter := &JSONFooter{} + err = json.Unmarshal(footer, jsonFooter) + if err != nil { + return nil, nil, err + } + peerKey, ok := auth.peers[jsonFooter.KID] + if !ok { + return nil, nil, fmt.Errorf("unknown peer key ID %s", jsonFooter.KID) + } + + msgSig, err := base64.RawURLEncoding.DecodeString(pieces[2]) + if err != nil { + return nil, nil, err + } + + msg, err := Verify(peerKey, msgSig, footer) + if err != nil { + return nil, nil, err + } + + jtoken := &JSONToken{} + err = json.Unmarshal(msg, jtoken) + if err != nil { + return nil, nil, err + } + + err = jtoken.Validate() + if err != nil { + return nil, nil, err + } + + data, err = base64.StdEncoding.DecodeString(jtoken.Data) + if err != nil { + return nil, nil, err + } + + return data, jsonFooter.Data, nil +} |
