Skip to content

Commit b28c7ff

Browse files
authored
Merge pull request #112 from silinternational/develop
Release -- more groundwork for credential rotation
2 parents 5c4f9a6 + a1e2ccf commit b28c7ff

24 files changed

+329
-243
lines changed

.github/workflows/test-deploy-publish.yml

Lines changed: 35 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -16,69 +16,71 @@ jobs:
1616
name: Tests
1717
runs-on: ubuntu-latest
1818
env:
19-
AWS_REGION: ${{ vars.AWS_REGION }}
20-
STG_AWS_ACCESS_KEY_ID: ${{ vars.STG_AWS_ACCESS_KEY_ID }}
21-
STG_AWS_SECRET_ACCESS_KEY: ${{ secrets.STG_AWS_SECRET_ACCESS_KEY }}
22-
PRD_AWS_ACCESS_KEY_ID: ${{ vars.PRD_AWS_ACCESS_KEY_ID }}
23-
PRD_AWS_SECRET_ACCESS_KEY: ${{ secrets.PRD_AWS_SECRET_ACCESS_KEY }}
19+
AWS_REGION: us-east-1
20+
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
21+
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
2422
steps:
2523
- name: Checkout code
2624
uses: actions/checkout@v4
25+
2726
- name: Test
28-
run: docker compose -f actions-services.yml run --rm test ./scripts/test.sh
27+
run: docker compose run app go test ./...
2928

3029
lint:
3130
name: Lint and Vulnerability Scan
3231
runs-on: ubuntu-latest
3332
timeout-minutes: ${{ fromJSON(vars.DEFAULT_JOB_TIMEOUT_MINUTES) }}
3433
steps:
35-
- uses: actions/checkout@v4
36-
- uses: actions/setup-go@v5
37-
with:
38-
go-version-file: 'go.mod'
39-
check-latest: true
40-
- name: golangci-lint
41-
uses: golangci/golangci-lint-action@v6
42-
with:
43-
version: latest
44-
- name: govulncheck
45-
run: |
46-
go install golang.org/x/vuln/cmd/govulncheck@latest
47-
govulncheck ./...
34+
- name: Checkout code
35+
uses: actions/checkout@v4
36+
37+
- uses: actions/setup-go@v5
38+
with:
39+
go-version-file: 'go.mod'
40+
check-latest: true
41+
42+
- name: golangci-lint
43+
uses: golangci/golangci-lint-action@v6
44+
with:
45+
version: latest
46+
47+
- name: govulncheck
48+
run: |
49+
go install golang.org/x/vuln/cmd/govulncheck@latest
50+
govulncheck ./...
4851
4952
deploy:
5053
name: Deploy to AWS Lambda
5154
needs: [ 'tests', 'lint' ]
5255
if: github.ref_name == 'main' || github.ref_name == 'develop'
56+
environment: ${{ github.ref_name }}
5357
runs-on: ubuntu-latest
5458
concurrency:
5559
group: deploy-${{ github.ref }}-${{ matrix.region }}
5660
cancel-in-progress: false
5761
strategy:
5862
matrix:
5963
region: [ us-east-1, us-west-2 ]
60-
env:
61-
AWS_REGION: ${{ vars.AWS_REGION }}
62-
STG_AWS_ACCESS_KEY_ID: ${{ vars.STG_AWS_ACCESS_KEY_ID }}
63-
STG_AWS_SECRET_ACCESS_KEY: ${{ secrets.STG_AWS_SECRET_ACCESS_KEY }}
64-
STG_LAMBDA_ROLE: ${{ vars.STG_LAMBDA_ROLE }}
65-
STG_API_KEY_TABLE: ${{ vars.STG_API_KEY_TABLE }}
66-
STG_WEBAUTHN_TABLE: ${{ vars.STG_WEBAUTHN_TABLE }}
67-
PRD_AWS_ACCESS_KEY_ID: ${{ vars.PRD_AWS_ACCESS_KEY_ID }}
68-
PRD_AWS_SECRET_ACCESS_KEY: ${{ secrets.PRD_AWS_SECRET_ACCESS_KEY }}
69-
PRD_LAMBDA_ROLE: ${{ vars.PRD_LAMBDA_ROLE }}
70-
PRD_API_KEY_TABLE: ${{ vars.PRD_API_KEY_TABLE }}
71-
PRD_WEBAUTHN_TABLE: ${{ vars.PRD_WEBAUTHN_TABLE }}
7264

7365
steps:
7466
- name: Checkout code
7567
uses: actions/checkout@v4
68+
7669
- name: Deploy
77-
run: docker compose -f actions-services.yml run --rm app ./scripts/deploy.sh ${{ matrix.region }}
70+
run: |
71+
docker compose run \
72+
-e LAMBDA_ROLE=${{ vars.LAMBDA_ROLE }} \
73+
-e API_KEY_TABLE=${{ vars.API_KEY_TABLE }} \
74+
-e TOTP_TABLE=${{ vars.TOTP_TABLE }} \
75+
-e WEBAUTHN_TABLE=${{ vars.WEBAUTHN_TABLE }} \
76+
-e AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }} \
77+
-e AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }} \
78+
app ./scripts/deploy.sh ${{ matrix.region }} ${{ vars.STAGE }}
7879
7980
build-and-publish:
8081
name: Build and Publish
8182
needs: [ 'tests', 'lint' ]
83+
if: github.ref_name == 'main' || github.ref_name == 'develop'
8284
runs-on: ubuntu-latest
8385
steps:
8486
- name: Checkout code
@@ -102,7 +104,7 @@ jobs:
102104
uses: docker/metadata-action@v5
103105
with:
104106
images: |
105-
${{ vars.IMAGE_NAME }}
107+
${{ vars.DOCKER_ORG }}/${{ github.event.repository.name }}
106108
ghcr.io/${{ github.repository }}
107109
tags: |
108110
type=ref,event=branch

Makefile

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ createwebauthntable:
3030
--attribute-definitions AttributeName=uuid,AttributeType=S \
3131
--key-schema AttributeName=uuid,KeyType=HASH
3232

33+
createtotptable:
34+
AWS_ENDPOINT=http://localhost:8000 AWS_DEFAULT_REGION=local AWS_ACCESS_KEY_ID=abc123 AWS_SECRET_ACCESS_KEY=abc123 AWS_PAGER="" aws dynamodb create-table \
35+
--table-name Totp \
36+
--billing-mode PAY_PER_REQUEST \
37+
--attribute-definitions AttributeName=uuid,AttributeType=S \
38+
--key-schema AttributeName=uuid,KeyType=HASH
39+
3340
# create ApiKey table with test key = EC7C2E16-5028-432F-8AF2-A79A64CF3BC1, secret = 1ED18444-7238-410B-A536-D6C15A3C
3441
createapikeytable:
3542
AWS_ENDPOINT=http://localhost:8000 AWS_DEFAULT_REGION=local AWS_ACCESS_KEY_ID=abc123 AWS_SECRET_ACCESS_KEY=abc123 AWS_PAGER="" aws dynamodb create-table \

actions-services.yml

Lines changed: 0 additions & 41 deletions
This file was deleted.

apikey.go

Lines changed: 57 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,12 @@ import (
88
"encoding/base64"
99
"encoding/hex"
1010
"encoding/json"
11+
"errors"
1112
"fmt"
1213
"io"
1314
"net/http"
1415
"time"
1516

16-
"github.com/pkg/errors"
17-
1817
"golang.org/x/crypto/bcrypt"
1918
)
2019

@@ -53,35 +52,33 @@ func (k *ApiKey) Hash() error {
5352
return err
5453
}
5554

56-
// IsCorrect returns true if and only if the given string is a match for HashedSecret
57-
func (k *ApiKey) IsCorrect(given string) (bool, error) {
55+
// IsCorrect returns true if and only if the key is active and the given string is a match for HashedSecret
56+
func (k *ApiKey) IsCorrect(given string) error {
57+
if k.ActivatedAt == 0 {
58+
return fmt.Errorf("key is not active: %s", k.Key)
59+
}
60+
5861
if given == "" {
59-
return false, errors.New("secret to compare cannot be empty")
62+
return errors.New("secret to compare cannot be empty")
6063
}
64+
6165
if k.HashedSecret == "" {
62-
return false, errors.New("cannot compare with empty hashed secret")
66+
return errors.New("cannot compare with empty hashed secret")
6367
}
6468

6569
err := bcrypt.CompareHashAndPassword([]byte(k.HashedSecret), []byte(given))
6670
if err != nil {
67-
return false, err
71+
return err
6872
}
6973

70-
return true, nil
74+
return nil
7175
}
7276

7377
// EncryptData uses the Secret to AES encrypt an arbitrary data block. It does not encrypt the key itself.
7478
func (k *ApiKey) EncryptData(plaintext []byte) ([]byte, error) {
75-
var sec []byte
76-
var err error
77-
sec, err = base64.StdEncoding.DecodeString(k.Secret)
78-
if err != nil {
79-
sec = []byte(k.Secret)
80-
}
81-
// create cipher block with api secret as aes key
82-
block, err := aes.NewCipher(sec)
79+
block, err := newCipherBlock(k.Secret)
8380
if err != nil {
84-
return []byte{}, err
81+
return nil, err
8582
}
8683

8784
// byte array to hold encrypted content
@@ -103,16 +100,9 @@ func (k *ApiKey) EncryptData(plaintext []byte) ([]byte, error) {
103100

104101
// DecryptData uses the Secret to AES decrypt an arbitrary data block. It does not decrypt the key itself.
105102
func (k *ApiKey) DecryptData(ciphertext []byte) ([]byte, error) {
106-
var sec []byte
107-
var err error
108-
sec, err = base64.StdEncoding.DecodeString(k.Secret)
109-
if err != nil {
110-
sec = []byte(k.Secret)
111-
}
112-
113-
block, err := aes.NewCipher(sec)
103+
block, err := newCipherBlock(k.Secret)
114104
if err != nil {
115-
return []byte{}, errors.Wrap(err, "failed to create new cipher")
105+
return nil, err
116106
}
117107

118108
// plaintext must be as long as ciphertext minus the length of the IV, which is the same as the AES block size
@@ -128,19 +118,34 @@ func (k *ApiKey) DecryptData(ciphertext []byte) ([]byte, error) {
128118
return plaintext, nil
129119
}
130120

131-
// DecryptLegacy uses the Secret to AES decrypt an arbitrary data block. This is intended only for legacy data such
132-
// as U2F keys.
133-
func (k *ApiKey) DecryptLegacy(ciphertext []byte) ([]byte, error) {
134-
var sec []byte
135-
var err error
136-
sec, err = base64.StdEncoding.DecodeString(k.Secret)
121+
// EncryptLegacy uses the Secret to AES encrypt an arbitrary data block. This is intended only for legacy data such
122+
// as U2F keys. The returned data is the Base64-encoded IV and the Base64-encoded cipher text separated by a colon.
123+
func (k *ApiKey) EncryptLegacy(plaintext []byte) ([]byte, error) {
124+
block, err := newCipherBlock(k.Secret)
137125
if err != nil {
138-
sec = []byte(k.Secret)
126+
return nil, err
139127
}
140128

141-
block, err := aes.NewCipher(sec)
129+
iv := make([]byte, aes.BlockSize)
130+
if _, err = io.ReadFull(rand.Reader, iv); err != nil {
131+
return nil, fmt.Errorf("failed to create random data for initialization vector: %w", err)
132+
}
133+
134+
ciphertext := make([]byte, len(plaintext))
135+
stream := cipher.NewCTR(block, iv)
136+
stream.XORKeyStream(ciphertext, plaintext)
137+
138+
ivBase64 := base64.StdEncoding.EncodeToString(iv)
139+
cipherBase64 := base64.StdEncoding.EncodeToString(ciphertext)
140+
return []byte(ivBase64 + ":" + cipherBase64), nil
141+
}
142+
143+
// DecryptLegacy uses the Secret to AES decrypt an arbitrary data block. This is intended only for legacy data such
144+
// as U2F keys.
145+
func (k *ApiKey) DecryptLegacy(ciphertext []byte) ([]byte, error) {
146+
block, err := newCipherBlock(k.Secret)
142147
if err != nil {
143-
return []byte{}, errors.Wrap(err, "failed to create new cipher")
148+
return nil, err
144149
}
145150

146151
// data was encrypted, then base64 encoded, then joined with a :, need to split
@@ -302,3 +307,20 @@ func NewApiKey(email string) (ApiKey, error) {
302307
}
303308
return key, nil
304309
}
310+
311+
// newCipherBlock creates a new cipher.Block from a base64-encoded AES key. If the string is not valid base64 data, it
312+
// will be interpreted as binary data.
313+
func newCipherBlock(key string) (cipher.Block, error) {
314+
var sec []byte
315+
var err error
316+
sec, err = base64.StdEncoding.DecodeString(key)
317+
if err != nil {
318+
sec = []byte(key)
319+
}
320+
321+
block, err := aes.NewCipher(sec)
322+
if err != nil {
323+
return nil, fmt.Errorf("failed to create new cipher: %w", err)
324+
}
325+
return block, nil
326+
}

0 commit comments

Comments
 (0)