parent
b8c534cd0d
commit
d19a4348df
@ -0,0 +1,20 @@
|
||||
Copyright © 2013 Keith Rarick
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
@ -0,0 +1,22 @@
|
||||
Fernet takes a user-provided *message* (an arbitrary sequence of
|
||||
bytes), a *key* (256 bits), and the current time, and produces a
|
||||
*token*, which contains the message in a form that can't be read
|
||||
or altered without the key.
|
||||
|
||||
This package is compatible with the other implementations at
|
||||
https://github.com/fernet. They can exchange tokens freely among
|
||||
each other.
|
||||
|
||||
Documentation: http://godoc.org/github.com/fernet/fernet-go
|
||||
|
||||
|
||||
INSTALL
|
||||
|
||||
$ go get github.com/fernet/fernet-go
|
||||
|
||||
|
||||
For more information and background, see the Fernet spec at
|
||||
https://github.com/fernet/spec.
|
||||
|
||||
Fernet is distributed under the terms of the MIT license.
|
||||
See the License file for details.
|
@ -0,0 +1,19 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/fernet/fernet-go"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
log.SetPrefix("fernet: ")
|
||||
|
||||
var key fernet.Key
|
||||
if err := key.Generate(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println(key.Encode())
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/fernet/fernet-go"
|
||||
)
|
||||
|
||||
const Usage = `Usage: fernet-sign ENV
|
||||
|
||||
fernet-sign encrypts and signs its input and prints the resulting token.
|
||||
It uses the key in environment variable ENV.`
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
log.SetPrefix("fernet: ")
|
||||
|
||||
if len(os.Args) != 2 {
|
||||
fmt.Fprintln(os.Stderr, Usage)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
key, err := fernet.DecodeKey(os.Getenv(os.Args[1]))
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
b, err := ioutil.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
t, err := fernet.EncryptAndSign(b, key)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
_, err = os.Stdout.Write(append(t, '\n'))
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}
|
@ -0,0 +1,168 @@
|
||||
// Package fernet takes a user-provided message (an arbitrary
|
||||
// sequence of bytes), a key (256 bits), and the current time,
|
||||
// and produces a token, which contains the message in a form
|
||||
// that can't be read or altered without the key.
|
||||
//
|
||||
// For more information and background, see the Fernet spec
|
||||
// at https://github.com/fernet/spec.
|
||||
//
|
||||
// Subdirectories in this package provide command-line tools
|
||||
// for working with Fernet keys and tokens.
|
||||
package fernet
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
version byte = 0x80
|
||||
tsOffset = 1
|
||||
ivOffset = tsOffset + 8
|
||||
payOffset = ivOffset + aes.BlockSize
|
||||
overhead = 1 + 8 + aes.BlockSize + sha256.Size // ver + ts + iv + hmac
|
||||
maxClockSkew = 60 * time.Second
|
||||
)
|
||||
|
||||
var encoding = base64.URLEncoding
|
||||
|
||||
// generates a token from msg, writes it into tok, and returns the
|
||||
// number of bytes generated, which is encodedLen(msg).
|
||||
// len(tok) must be >= encodedLen(len(msg))
|
||||
func gen(tok, msg, iv []byte, ts time.Time, k *Key) int {
|
||||
tok[0] = version
|
||||
binary.BigEndian.PutUint64(tok[tsOffset:], uint64(ts.Unix()))
|
||||
copy(tok[ivOffset:], iv)
|
||||
p := tok[payOffset:]
|
||||
n := pad(p, msg, aes.BlockSize)
|
||||
bc, _ := aes.NewCipher(k.cryptBytes())
|
||||
cipher.NewCBCEncrypter(bc, iv).CryptBlocks(p[:n], p[:n])
|
||||
genhmac(p[n:n], tok[:payOffset+n], k.signBytes())
|
||||
return payOffset + n + sha256.Size
|
||||
}
|
||||
|
||||
// token length for input msg of length n, not including base64
|
||||
func encodedLen(n int) int {
|
||||
const k = aes.BlockSize
|
||||
return n/k*k + k + overhead
|
||||
}
|
||||
|
||||
// max msg length for tok of length n, for binary token (no base64)
|
||||
// upper bound; not exact
|
||||
func decodedLen(n int) int {
|
||||
return n - overhead
|
||||
}
|
||||
|
||||
// if msg is nil, decrypts in place and returns a slice of tok.
|
||||
func verify(msg, tok []byte, ttl time.Duration, now time.Time, k *Key) []byte {
|
||||
if len(tok) < 1 || tok[0] != version {
|
||||
return nil
|
||||
}
|
||||
ts := time.Unix(int64(binary.BigEndian.Uint64(tok[1:])), 0)
|
||||
if ttl >= 0 && (now.After(ts.Add(ttl)) || ts.After(now.Add(maxClockSkew))) {
|
||||
return nil
|
||||
}
|
||||
n := len(tok) - sha256.Size
|
||||
var hmac [sha256.Size]byte
|
||||
genhmac(hmac[:0], tok[:n], k.signBytes())
|
||||
if subtle.ConstantTimeCompare(tok[n:], hmac[:]) != 1 {
|
||||
return nil
|
||||
}
|
||||
pay := tok[payOffset : len(tok)-sha256.Size]
|
||||
if len(pay)%aes.BlockSize != 0 {
|
||||
return nil
|
||||
}
|
||||
if msg != nil {
|
||||
copy(msg, pay)
|
||||
pay = msg
|
||||
}
|
||||
bc, _ := aes.NewCipher(k.cryptBytes())
|
||||
iv := tok[9:][:aes.BlockSize]
|
||||
cipher.NewCBCDecrypter(bc, iv).CryptBlocks(pay, pay)
|
||||
return unpad(pay)
|
||||
}
|
||||
|
||||
// Pads p to a multiple of k using PKCS #7 standard block padding.
|
||||
// See http://tools.ietf.org/html/rfc5652#section-6.3.
|
||||
func pad(q, p []byte, k int) int {
|
||||
n := len(p)/k*k + k
|
||||
copy(q, p)
|
||||
c := byte(n - len(p))
|
||||
for i := len(p); i < n; i++ {
|
||||
q[i] = c
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// Removes PKCS #7 standard block padding from p.
|
||||
// See http://tools.ietf.org/html/rfc5652#section-6.3.
|
||||
// This function is the inverse of pad.
|
||||
// If the padding is not well-formed, unpad returns nil.
|
||||
func unpad(p []byte) []byte {
|
||||
c := p[len(p)-1]
|
||||
for i := len(p) - int(c); i < len(p); i++ {
|
||||
if i < 0 || p[i] != c {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return p[:len(p)-int(c)]
|
||||
}
|
||||
|
||||
func b64enc(src []byte) []byte {
|
||||
dst := make([]byte, encoding.EncodedLen(len(src)))
|
||||
encoding.Encode(dst, src)
|
||||
return dst
|
||||
}
|
||||
|
||||
func b64dec(src []byte) []byte {
|
||||
dst := make([]byte, encoding.DecodedLen(len(src)))
|
||||
n, err := encoding.Decode(dst, src)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return dst[:n]
|
||||
}
|
||||
|
||||
func genhmac(q, p, k []byte) {
|
||||
h := hmac.New(sha256.New, k)
|
||||
h.Write(p)
|
||||
h.Sum(q)
|
||||
}
|
||||
|
||||
// EncryptAndSign encrypts and signs msg with key k and returns the resulting
|
||||
// fernet token. If msg contains text, the text should be encoded
|
||||
// with UTF-8 to follow fernet convention.
|
||||
func EncryptAndSign(msg []byte, k *Key) (tok []byte, err error) {
|
||||
iv := make([]byte, aes.BlockSize)
|
||||
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b := make([]byte, encodedLen(len(msg)))
|
||||
n := gen(b, msg, iv, time.Now(), k)
|
||||
tok = make([]byte, encoding.EncodedLen(n))
|
||||
encoding.Encode(tok, b[:n])
|
||||
return tok, nil
|
||||
}
|
||||
|
||||
// VerifyAndDecrypt verifies that tok is a valid fernet token that was signed
|
||||
// with a key in k at most ttl time ago only if ttl is greater than zero.
|
||||
// Returns the message contained in tok if tok is valid, otherwise nil.
|
||||
func VerifyAndDecrypt(tok []byte, ttl time.Duration, k []*Key) (msg []byte) {
|
||||
b := make([]byte, encoding.DecodedLen(len(tok)))
|
||||
n, _ := encoding.Decode(b, tok)
|
||||
for _, k1 := range k {
|
||||
msg = verify(nil, b[:n], ttl, time.Now(), k1)
|
||||
if msg != nil {
|
||||
return msg
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
[
|
||||
{
|
||||
"token": "gAAAAAAdwJ6wAAECAwQFBgcICQoLDA0ODy021cpGVWKZ_eEwCGM4BLLF_5CV9dOPmrhuVUPgJobwOz7JcbmrR64jVmpU4IwqDA==",
|
||||
"now": "1985-10-26T01:20:00-07:00",
|
||||
"iv": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
|
||||
"src": "hello",
|
||||
"secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4="
|
||||
}
|
||||
]
|
@ -0,0 +1,58 @@
|
||||
[
|
||||
{
|
||||
"desc": "incorrect mac",
|
||||
"token": "gAAAAAAdwJ6xAAECAwQFBgcICQoLDA0OD3HkMATM5lFqGaerZ-fWPAl1-szkFVzXTuGb4hR8AKtwcaX1YdykQUFBQUFBQUFBQQ==",
|
||||
"now": "1985-10-26T01:20:01-07:00",
|
||||
"ttl_sec": 60,
|
||||
"secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4="
|
||||
},
|
||||
{
|
||||
"desc": "too short",
|
||||
"token": "gAAAAAAdwJ6xAAECAwQFBgcICQoLDA0OD3HkMATM5lFqGaerZ-fWPA==",
|
||||
"now": "1985-10-26T01:20:01-07:00",
|
||||
"ttl_sec": 60,
|
||||
"secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4="
|
||||
},
|
||||
{
|
||||
"desc": "invalid base64",
|
||||
"token": "%%%%%%%%%%%%%AECAwQFBgcICQoLDA0OD3HkMATM5lFqGaerZ-fWPAl1-szkFVzXTuGb4hR8AKtwcaX1YdykRtfsH-p1YsUD2Q==",
|
||||
"now": "1985-10-26T01:20:01-07:00",
|
||||
"ttl_sec": 60,
|
||||
"secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4="
|
||||
},
|
||||
{
|
||||
"desc": "payload size not multiple of block size",
|
||||
"token": "gAAAAAAdwJ6xAAECAwQFBgcICQoLDA0OD3HkMATM5lFqGaerZ-fWPOm73QeoCk9uGib28Xe5vz6oxq5nmxbx_v7mrfyudzUm",
|
||||
"now": "1985-10-26T01:20:01-07:00",
|
||||
"ttl_sec": 60,
|
||||
"secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4="
|
||||
},
|
||||
{
|
||||
"desc": "payload padding error",
|
||||
"token": "gAAAAAAdwJ6xAAECAwQFBgcICQoLDA0ODz4LEpdELGQAad7aNEHbf-JkLPIpuiYRLQ3RtXatOYREu2FWke6CnJNYIbkuKNqOhw==",
|
||||
"now": "1985-10-26T01:20:01-07:00",
|
||||
"ttl_sec": 60,
|
||||
"secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4="
|
||||
},
|
||||
{
|
||||
"desc": "far-future TS (unacceptable clock skew)",
|
||||
"token": "gAAAAAAdwStRAAECAwQFBgcICQoLDA0OD3HkMATM5lFqGaerZ-fWPAnja1xKYyhd-Y6mSkTOyTGJmw2Xc2a6kBd-iX9b_qXQcw==",
|
||||
"now": "1985-10-26T01:20:01-07:00",
|
||||
"ttl_sec": 60,
|
||||
"secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4="
|
||||
},
|
||||
{
|
||||
"desc": "expired TTL",
|
||||
"token": "gAAAAAAdwJ6xAAECAwQFBgcICQoLDA0OD3HkMATM5lFqGaerZ-fWPAl1-szkFVzXTuGb4hR8AKtwcaX1YdykRtfsH-p1YsUD2Q==",
|
||||
"now": "1985-10-26T01:21:31-07:00",
|
||||
"ttl_sec": 60,
|
||||
"secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4="
|
||||
},
|
||||
{
|
||||
"desc": "incorrect IV (causes padding error)",
|
||||
"token": "gAAAAAAdwJ6xBQECAwQFBgcICQoLDA0OD3HkMATM5lFqGaerZ-fWPAkLhFLHpGtDBRLRTZeUfWgHSv49TF2AUEZ1TIvcZjK1zQ==",
|
||||
"now": "1985-10-26T01:20:01-07:00",
|
||||
"ttl_sec": 60,
|
||||
"secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4="
|
||||
}
|
||||
]
|
@ -0,0 +1,91 @@
|
||||
package fernet
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
var (
|
||||
errKeyLen = errors.New("fernet: key decodes to wrong size")
|
||||
errNoKeys = errors.New("fernet: no keys provided")
|
||||
)
|
||||
|
||||
// Key represents a key.
|
||||
type Key [32]byte
|
||||
|
||||
func (k *Key) cryptBytes() []byte {
|
||||
return k[len(k)/2:]
|
||||
}
|
||||
|
||||
func (k *Key) signBytes() []byte {
|
||||
return k[:len(k)/2]
|
||||
}
|
||||
|
||||
// Generate initializes k with pseudorandom data from package crypto/rand.
|
||||
func (k *Key) Generate() error {
|
||||
_, err := io.ReadFull(rand.Reader, k[:])
|
||||
return err
|
||||
}
|
||||
|
||||
// Encode returns the URL-safe base64 encoding of k.
|
||||
func (k *Key) Encode() string {
|
||||
return encoding.EncodeToString(k[:])
|
||||
}
|
||||
|
||||
// DecodeKey decodes a key from s and returns it. The key can be in
|
||||
// hexadecimal, standard base64, or URL-safe base64.
|
||||
func DecodeKey(s string) (*Key, error) {
|
||||
var b []byte
|
||||
var err error
|
||||
if s == "" {
|
||||
return nil, errors.New("empty key")
|
||||
}
|
||||
if len(s) == hex.EncodedLen(len(Key{})) {
|
||||
b, err = hex.DecodeString(s)
|
||||
} else {
|
||||
b, err = base64.StdEncoding.DecodeString(s)
|
||||
if err != nil {
|
||||
b, err = base64.URLEncoding.DecodeString(s)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(b) != len(Key{}) {
|
||||
return nil, errKeyLen
|
||||
}
|
||||
k := new(Key)
|
||||
copy(k[:], b)
|
||||
return k, nil
|
||||
}
|
||||
|
||||
// DecodeKeys decodes each element of a using DecodeKey and returns the
|
||||
// resulting keys. Requires at least one key.
|
||||
func DecodeKeys(a ...string) ([]*Key, error) {
|
||||
if len(a) == 0 {
|
||||
return nil, errNoKeys
|
||||
}
|
||||
var err error
|
||||
ks := make([]*Key, len(a))
|
||||
for i, s := range a {
|
||||
ks[i], err = DecodeKey(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return ks, nil
|
||||
}
|
||||
|
||||
// MustDecodeKeys is like DecodeKeys, but panics if an error occurs.
|
||||
// It simplifies safe initialization of global variables holding
|
||||
// keys.
|
||||
func MustDecodeKeys(a ...string) []*Key {
|
||||
k, err := DecodeKeys(a...)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return k
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
[
|
||||
{
|
||||
"token": "gAAAAAAdwJ6wAAECAwQFBgcICQoLDA0ODy021cpGVWKZ_eEwCGM4BLLF_5CV9dOPmrhuVUPgJobwOz7JcbmrR64jVmpU4IwqDA==",
|
||||
"now": "1985-10-26T01:20:01-07:00",
|
||||
"ttl_sec": 60,
|
||||
"src": "hello",
|
||||
"secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4="
|
||||
},
|
||||
{
|
||||
"token": "gAAAAAAdwJ6wAAECAwQFBgcICQoLDA0ODy021cpGVWKZ_eEwCGM4BLLF_5CV9dOPmrhuVUPgJobwOz7JcbmrR64jVmpU4IwqDA==",
|
||||
"now": "1985-10-26T01:20:01-07:00",
|
||||
"ttl_sec": -1,
|
||||
"src": "hello",
|
||||
"secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4="
|
||||
}
|
||||
]
|
Loading…
Reference in new issue