Skip to content

Commit 15b79f6

Browse files
authored
Initial JWE Support (#1)
Signed-off-by: ortyomka <[email protected]>
1 parent 613321c commit 15b79f6

File tree

14 files changed

+693
-1
lines changed

14 files changed

+693
-1
lines changed

.github/workflows/codeql-analysis.yml

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# For most projects, this workflow file will not need changing; you simply need
2+
# to commit it to your repository.
3+
#
4+
# You may wish to alter this file to override the set of languages analyzed,
5+
# or to provide custom queries or build logic.
6+
#
7+
# ******** NOTE ********
8+
# We have attempted to detect the languages in your repository. Please check
9+
# the `language` matrix defined below to confirm you have the correct set of
10+
# supported CodeQL languages.
11+
#
12+
name: "CodeQL"
13+
14+
on:
15+
push:
16+
branches: [ main ]
17+
# pull_request:
18+
# The branches below must be a subset of the branches above
19+
# branches: [ main ]
20+
schedule:
21+
- cron: '31 10 * * 5'
22+
23+
jobs:
24+
analyze:
25+
name: Analyze
26+
runs-on: ubuntu-latest
27+
permissions:
28+
actions: read
29+
contents: read
30+
security-events: write
31+
32+
strategy:
33+
fail-fast: false
34+
matrix:
35+
language: [ 'go' ]
36+
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
37+
# Learn more:
38+
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
39+
40+
steps:
41+
- name: Checkout repository
42+
uses: actions/checkout@v2
43+
44+
# Initializes the CodeQL tools for scanning.
45+
- name: Initialize CodeQL
46+
uses: github/codeql-action/init@v1
47+
with:
48+
languages: ${{ matrix.language }}
49+
# If you wish to specify custom queries, you can do so here or in a config file.
50+
# By default, queries listed here will override any specified in a config file.
51+
# Prefix the list here with "+" to use these queries and those in the config file.
52+
# queries: ./path/to/local/query, your-org/your-repo/queries@main
53+
54+
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
55+
# If this step fails, then you should remove it and run the build manually (see below)
56+
- name: Autobuild
57+
uses: github/codeql-action/autobuild@v1
58+
59+
# ℹ️ Command-line programs to run using the OS shell.
60+
# 📚 https://git.io/JvXDl
61+
62+
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
63+
# and modify them (or add more) to build your code if your project
64+
# uses a compiled language
65+
66+
#- run: |
67+
# make bootstrap
68+
# make release
69+
70+
- name: Perform CodeQL Analysis
71+
uses: github/codeql-action/analyze@v1

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.DS_Store
2+
bin
3+
.idea/
4+

LICENSE

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Copyright (c) 2022 golang-jwt maintainers
2+
3+
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:
4+
5+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6+
7+
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.
8+

aesgcm.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package jwe
2+
3+
import (
4+
"crypto/aes"
5+
"crypto/cipher"
6+
"errors"
7+
"io"
8+
)
9+
10+
var (
11+
ErrInvalidKeySize = errors.New("invalid key size")
12+
ErrInvalidTagSize = errors.New("invalid tag size")
13+
ErrInvalidNonceSize = errors.New("invalid nonce size")
14+
ErrUnsupportedEncryptionType = errors.New("unsupported encryption type")
15+
)
16+
17+
const TagSizeAESGCM = 16
18+
19+
type EncryptionType string
20+
21+
var EncryptionTypeA256GCM = EncryptionType("A256GCM")
22+
23+
type cipherAESGCM struct {
24+
keySize int
25+
getAEAD func(key []byte) (cipher.AEAD, error)
26+
}
27+
28+
func (ci cipherAESGCM) encrypt(key, aad, plaintext []byte) (iv []byte, ciphertext []byte, tag []byte, err error) {
29+
if len(key) != ci.keySize {
30+
return nil, nil, nil, ErrInvalidKeySize
31+
}
32+
33+
aead, err := ci.getAEAD(key)
34+
if err != nil {
35+
return nil, nil, nil, err
36+
}
37+
38+
iv = make([]byte, aead.NonceSize())
39+
_, err = io.ReadFull(RandReader, iv)
40+
if err != nil {
41+
return nil, nil, nil, err
42+
}
43+
44+
res := aead.Seal(nil, iv, plaintext, aad)
45+
tagIndex := len(res) - TagSizeAESGCM
46+
47+
return iv, res[:tagIndex], res[tagIndex:], nil
48+
}
49+
50+
func (ci cipherAESGCM) decrypt(key, aad, iv []byte, ciphertext []byte, tag []byte) ([]byte, error) {
51+
if len(key) != ci.keySize {
52+
return nil, ErrInvalidKeySize
53+
}
54+
55+
if len(tag) != TagSizeAESGCM {
56+
return nil, ErrInvalidTagSize
57+
}
58+
59+
aead, err := ci.getAEAD(key)
60+
if err != nil {
61+
return nil, err
62+
}
63+
64+
if len(iv) != aead.NonceSize() {
65+
return nil, ErrInvalidNonceSize
66+
}
67+
68+
return aead.Open(nil, iv, append(ciphertext, tag...), aad)
69+
}
70+
71+
func newAESGCM(keySize int) *cipherAESGCM {
72+
return &cipherAESGCM{
73+
keySize: keySize,
74+
getAEAD: func(key []byte) (cipher.AEAD, error) {
75+
aesCipher, err := aes.NewCipher(key)
76+
if err != nil {
77+
return nil, err
78+
}
79+
80+
return cipher.NewGCM(aesCipher)
81+
},
82+
}
83+
}
84+
85+
func getCipher(alg EncryptionType) (*cipherAESGCM, error) {
86+
switch alg {
87+
case EncryptionTypeA256GCM:
88+
return newAESGCM(32), nil
89+
default:
90+
return nil, ErrUnsupportedEncryptionType
91+
}
92+
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
module github.com/golang-jwt/jwe
22

3-
go 1.19
3+
go 1.18

jwe.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package jwe
2+
3+
import (
4+
"encoding/base64"
5+
"encoding/json"
6+
"strings"
7+
)
8+
9+
// NewJWE creates a new JWE token.
10+
// The plaintext will be encrypted with the method using a Content Encryption Key (cek).
11+
// The cek will be encrypted with the alg using the key.
12+
func NewJWE(alg KeyAlgorithm, key interface{}, method EncryptionType, plaintext []byte) (*jwe, error) {
13+
jwe := &jwe{}
14+
15+
jwe.protected.Enc = method
16+
chipher, err := getCipher(method)
17+
if err != nil {
18+
return nil, err
19+
}
20+
21+
// Generate a random Content Encryption Key (CEK).
22+
cek, err := generateKey(chipher.keySize)
23+
if err != nil {
24+
return nil, err
25+
}
26+
27+
// Encrypt the CEK with the recipient's public key to produce the JWE Encrypted Key.
28+
jwe.protected.Alg = alg
29+
encrypter, err := createEncrypter(key)
30+
if err != nil {
31+
return nil, err
32+
}
33+
jwe.recipientKey, err = encrypter.Encrypt(cek, alg)
34+
if err != nil {
35+
return nil, err
36+
}
37+
38+
// Serialize Authenticated Data
39+
rawProtected, err := json.Marshal(jwe.protected)
40+
if err != nil {
41+
return nil, err
42+
}
43+
rawProtectedBase64 := base64.RawURLEncoding.EncodeToString(rawProtected)
44+
45+
// Perform authenticated encryption on the plaintext
46+
jwe.iv, jwe.ciphertext, jwe.tag, err = chipher.encrypt(cek, []byte(rawProtectedBase64), plaintext)
47+
if err != nil {
48+
return nil, err
49+
}
50+
51+
return jwe, nil
52+
}
53+
54+
// jwe internal structure represents JWE in unmarshalling format.
55+
type jwe struct {
56+
// protected fields: alg - algorithm to encrypt a key and enc - algorithm to encrypt text.
57+
protected struct {
58+
Alg KeyAlgorithm `json:"alg,omitempty"`
59+
Enc EncryptionType `json:"enc,omitempty"`
60+
}
61+
62+
// recipientKey field is the key encrypted.
63+
recipientKey []byte
64+
65+
// iv field is initialization vector.
66+
iv []byte
67+
68+
// ciphertext filed is text encrypted by the enc with the key.
69+
ciphertext []byte
70+
71+
// tag field is authentication tag.
72+
tag []byte
73+
}
74+
75+
// CompactSerialize serialize JWE to compact form.
76+
// https://datatracker.ietf.org/doc/html/rfc7516#section-3.1
77+
func (jwe *jwe) CompactSerialize() (string, error) {
78+
rawProtected, err := json.Marshal(jwe.protected)
79+
if err != nil {
80+
return "", err
81+
}
82+
83+
protected := base64.RawURLEncoding.EncodeToString(rawProtected)
84+
encryptedKey := base64.RawURLEncoding.EncodeToString(jwe.recipientKey)
85+
iv := base64.RawURLEncoding.EncodeToString(jwe.iv)
86+
ciphertext := base64.RawURLEncoding.EncodeToString(jwe.ciphertext)
87+
tag := base64.RawURLEncoding.EncodeToString(jwe.tag)
88+
89+
return strings.Join([]string{protected, encryptedKey, iv, ciphertext, tag}, "."), nil
90+
}

jwe_decrypt.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package jwe
2+
3+
import (
4+
"encoding/base64"
5+
"encoding/json"
6+
"errors"
7+
)
8+
9+
var (
10+
ErrMissingEncHeader = errors.New("missing \"enc\" header")
11+
ErrMissingAlgHeader = errors.New("missing \"alg\" header")
12+
)
13+
14+
// Decrypt decrypts JWE ciphertext with the key
15+
func (jwe jwe) Decrypt(key interface{}) ([]byte, error) {
16+
17+
method := jwe.protected.Enc
18+
if len(method) == 0 {
19+
return nil, ErrMissingEncHeader
20+
}
21+
cipher, err := getCipher(method)
22+
if err != nil {
23+
return nil, err
24+
}
25+
26+
alg := jwe.protected.Alg
27+
if len(alg) == 0 {
28+
return nil, ErrMissingAlgHeader
29+
}
30+
decrypter, err := createDecrypter(key)
31+
if err != nil {
32+
return nil, err
33+
}
34+
// Decrypt JWE Encrypted Key with the recipient's private key to produce CEK.
35+
cek, err := decrypter.Decrypt(jwe.recipientKey, alg)
36+
if err != nil {
37+
return nil, err
38+
}
39+
40+
// Serialize Authenticated Data
41+
rawProtected, err := json.Marshal(jwe.protected)
42+
if err != nil {
43+
return nil, err
44+
}
45+
rawProtectedBase64 := base64.RawURLEncoding.EncodeToString(rawProtected)
46+
47+
// Perform authenticated decryption on the ciphertext
48+
data, err := cipher.decrypt(cek, []byte(rawProtectedBase64), jwe.iv, jwe.ciphertext, jwe.tag)
49+
50+
return data, err
51+
}

jwe_parse.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package jwe
2+
3+
import (
4+
"encoding/base64"
5+
"encoding/json"
6+
"errors"
7+
"strings"
8+
)
9+
10+
func ParseEncrypted(input string) (*jwe, error) {
11+
12+
if strings.HasPrefix(input, "{") {
13+
// TODO (ortyomka): Add full version support
14+
return nil, errors.New("don't support full JWE")
15+
}
16+
17+
return parseEncryptedCompact(input)
18+
}
19+
20+
func parseEncryptedCompact(input string) (*jwe, error) {
21+
parts := strings.Split(input, ".")
22+
23+
if len(parts) != 5 {
24+
return nil, errors.New("encrypted token contains an invalid number of segments")
25+
}
26+
27+
jwe := &jwe{}
28+
29+
rawProtected, err := base64.RawURLEncoding.DecodeString(parts[0])
30+
if err != nil {
31+
return nil, err
32+
}
33+
34+
if len(rawProtected) == 0 {
35+
return nil, errors.New("protected headers are empty")
36+
}
37+
38+
err = json.Unmarshal(rawProtected, &jwe.protected)
39+
if err != nil {
40+
return nil, errors.New("protected headers are not in JSON format")
41+
}
42+
43+
jwe.recipientKey, err = base64.RawURLEncoding.DecodeString(parts[1])
44+
if err != nil {
45+
return nil, err
46+
}
47+
jwe.iv, err = base64.RawURLEncoding.DecodeString(parts[2])
48+
if err != nil {
49+
return nil, err
50+
}
51+
jwe.ciphertext, err = base64.RawURLEncoding.DecodeString(parts[3])
52+
if err != nil {
53+
return nil, err
54+
}
55+
jwe.tag, err = base64.RawURLEncoding.DecodeString(parts[4])
56+
if err != nil {
57+
return nil, err
58+
}
59+
60+
return jwe, nil
61+
}

0 commit comments

Comments
 (0)