summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShulhan <m.shulhan@gmail.com>2020-09-09 05:46:36 +0700
committerShulhan <m.shulhan@gmail.com>2020-09-09 08:05:21 +0700
commit18ab9aad4c3a99822b9dfdc37a05dae12f4fb05d (patch)
treef5b0e438ae06708c1f4a2ba74e2250eed038f0b6
parent9af6d3604d103c55975b70f281b47d11cc9da63d (diff)
downloadpakakeh.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.adoc8
-rw-r--r--README.adoc3
-rw-r--r--_doc/CHANGELOG.html81
-rw-r--r--_doc/index.html4
-rw-r--r--lib/paseto/example_local_mode_test.go37
-rw-r--r--lib/paseto/example_public_mode_test.go71
-rw-r--r--lib/paseto/json_footer.go10
-rw-r--r--lib/paseto/json_token.go43
-rw-r--r--lib/paseto/key.go24
-rw-r--r--lib/paseto/local_mode.go46
-rw-r--r--lib/paseto/paseto.go318
-rw-r--r--lib/paseto/paseto_test.go287
-rw-r--r--lib/paseto/public_mode.go131
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
+}