more implementation of runtime unlock
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2023-12-28 11:14:16 +11:00
parent 9203e09d2d
commit 484acd1822
7 changed files with 115 additions and 43 deletions

View File

@@ -10,7 +10,7 @@ This isn't super secure, probably not even as secure as Hashicorp Vault running
## Installation ## Installation
1. Copy binary to chosen location, eg /srv/ccsecrets 1. Copy binary to chosen location, eg /srv/smt/smt
2. Create .env file in same directory as binary, populate as per Configuration section below 2. Create .env file in same directory as binary, populate as per Configuration section below
3. Create systemd service definition 3. Create systemd service definition
4. Start service 4. Start service
@@ -26,7 +26,7 @@ This isn't super secure, probably not even as secure as Hashicorp Vault running
| TOKEN_HOUR_LIFESPAN | Number of hours that the JWT token returned at login is valid | 12 | No default specified, must define this value | | TOKEN_HOUR_LIFESPAN | Number of hours that the JWT token returned at login is valid | 12 | No default specified, must define this value |
| API_SECRET | Secret to use when generating JWT token | 3c55990bd479322e2053db3a8 | No default specified, must define this value | | API_SECRET | Secret to use when generating JWT token | 3c55990bd479322e2053db3a8 | No default specified, must define this value |
| INITIAL_PASSWORD | Password to set for builtin Administrator account created when first started, can remove this value after first start. Can specify in plaintext or bcrypt hash | $2a$10$s39a82wrRAdOJVZEkkrSReVnXprz5mxU30ZBO.dHPYTncQCsUD9ce | password | INITIAL_PASSWORD | Password to set for builtin Administrator account created when first started, can remove this value after first start. Can specify in plaintext or bcrypt hash | $2a$10$s39a82wrRAdOJVZEkkrSReVnXprz5mxU30ZBO.dHPYTncQCsUD9ce | password
| SECRETS_KEY | Key to use for AES256 GCM encryption. Must be exactly 32 bytes | AES256Key-32Characters1234567890 | No default specified, must define this value | | SECRETS_KEY | Key to use for AES256 GCM encryption. Must be exactly 32 bytes | AES256Key-32Characters1234567890 | No default specified, must define this value or use /api/unlock at runtime |
If the TLS certificate and key files cannot be located in the specified location, a self signed certificate will be generated with a 1 year validity period. If the TLS certificate and key files cannot be located in the specified location, a self signed certificate will be generated with a 1 year validity period.
@@ -51,6 +51,18 @@ WantedBy=multi-user.target
``` ```
## API ## API
### Unlock
POST `/api/unlock`
Data
```
{
"secretKey": "Example32ByteSecretKey0123456789"
}
```
If the SECRETS_KEY environment variable is not defined, this API call to unlock stored secrets must be performed after initial startup of SMT. Storing/retrieval of secrets will not succeed until this API call has been made.
### User Operations ### User Operations
#### Register #### Register

View File

@@ -1,6 +1,41 @@
package controllers package controllers
import "github.com/gin-gonic/gin" import (
"log"
"net/http"
"smt/models"
"github.com/gin-gonic/gin"
)
type UnlockInput struct {
SecretKey string `json:"secretKey"`
}
// receive secret key and store it using the Key model
func Unlock(c *gin.Context) { func Unlock(c *gin.Context) {
var input UnlockInput
// Validate the input matches our struct
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
log.Printf("Unlock received JSON input '%v'\n", input)
// check that the key is 32 bytes long
if len(input.SecretKey) != 32 {
c.JSON(http.StatusBadRequest, gin.H{"error": "secret key provided is invalid, must be exactly 32 bytes long"})
return
}
err := models.ReceiveKey(input.SecretKey)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err})
return
}
// output results as json
c.JSON(http.StatusOK, gin.H{"message": "success", "data": "secret key successfully processed"})
} }

13
main.go
View File

@@ -49,14 +49,17 @@ func main() {
log.SetOutput(logfileWriter) log.SetOutput(logfileWriter)
log.Printf("SMT starting execution. Built on %s from sha1 %s\n", buildTime, sha1ver) log.Printf("SMT starting execution. Built on %s from sha1 %s\n", buildTime, sha1ver)
// Set secrets key from .env file
keyString = os.Getenv("SECRETS_KEY")
// Initiate connection to sqlite and make sure our schema is up to date // Initiate connection to sqlite and make sure our schema is up to date
models.ConnectDatabase() models.ConnectDatabase()
// let the models package know our secrets key // Set secrets key from .env file
models.LoadSecretKey(keyString) keyString = os.Getenv("SECRETS_KEY")
if keyString != "" {
// Key was defined in environment variable, let the models package know our secrets key
log.Println("Found secret key in environment variable")
models.ReceiveKey(keyString)
}
// Create context that listens for the interrupt signal from the OS. // Create context that listens for the interrupt signal from the OS.
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)

View File

@@ -1,12 +0,0 @@
package models
type Unlock struct {
SecretsKey string `json:"secrets"`
}
func (u *Unlock) UnlockSecrets() (*Unlock, error) {
// Receive secrets key and store in memory somehow
return u, nil
}

30
models/key.go Normal file
View File

@@ -0,0 +1,30 @@
package models
import "errors"
// TODO: Look at using shamir's secret sharing to distribute components of the secret key
var secretKey []byte
var secretReceived bool
func ReceiveKey(key string) error {
// confirm that the key is 32 bytes long exactly
if len(key) != 32 {
return errors.New("secret key provided is not exactly 32 bytes long")
}
// Store the secret key so that we can access it when encrypting/decrypting
secretKey = []byte(key)
secretReceived = true
return nil
}
func ProvideKey() ([]byte, error) {
// Provide the key when needed to decrypt/encrypt stored secrets
if secretReceived {
return secretKey, nil
} else {
return nil, errors.New("secret key has not been received")
}
}

View File

@@ -156,42 +156,48 @@ func (s *Secret) UpdateSecret() (*Secret, error) {
func (s *Secret) EncryptSecret() (*Secret, error) { func (s *Secret) EncryptSecret() (*Secret, error) {
//keyString := os.Getenv("SECRETS_KEY") //keyString := os.Getenv("SECRETS_KEY")
keyString := secretKey //keyString := secretKey
// The key argument should be the AES key, either 16 or 32 bytes // The key argument should be the AES key, either 16 or 32 bytes
// to select AES-128 or AES-256. // to select AES-128 or AES-256.
key := []byte(keyString) //key := []byte(keyString)
key, err := ProvideKey()
if err != nil {
return s, err
}
plaintext := []byte(s.Secret) plaintext := []byte(s.Secret)
//log.Printf("EncryptSecret applying key '%v' of length '%d' to plaintext secret '%s'\n", key, len(key), s.Secret) log.Printf("EncryptSecret applying key '%v' of length '%d' to plaintext secret '%s'\n", key, len(key), s.Secret)
// TODO : move block and aesgcm generation to separate function since the identical code is used for encrypt and decrypt
block, err := aes.NewCipher(key) block, err := aes.NewCipher(key)
if err != nil { if err != nil {
log.Printf("EncryptSecret NewCipher error '%s'\n", err) log.Printf("EncryptSecret NewCipher error '%s'\n", err)
return s, err return s, err
} }
aesgcm, err := cipher.NewGCM(block)
if err != nil {
log.Printf("EncryptSecret NewGCM error '%s'\n", err)
return s, err
}
// Never use more than 2^32 random nonces with a given key because of the risk of a repeat. // Never use more than 2^32 random nonces with a given key because of the risk of a repeat.
nonce := make([]byte, nonceSize) nonce := make([]byte, nonceSize)
if _, err := io.ReadFull(rand.Reader, nonce); err != nil { if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
log.Printf("EncryptSecret nonce generation error '%s'\n", err) log.Printf("EncryptSecret nonce generation error '%s'\n", err)
return s, err return s, err
} }
//log.Printf("EncryptSecret random nonce value is '%x'\n", nonce) log.Printf("EncryptSecret random nonce value is '%x'\n", nonce)
aesgcm, err := cipher.NewGCM(block)
if err != nil {
log.Printf("EncryptSecret NewGCM error '%s'\n", err)
return s, err
}
ciphertext := aesgcm.Seal(nil, nonce, plaintext, nil) ciphertext := aesgcm.Seal(nil, nonce, plaintext, nil)
//log.Printf("EncryptSecret generated ciphertext '%x''\n", ciphertext) log.Printf("EncryptSecret generated ciphertext '%x''\n", ciphertext)
// Create a new slice to store nonce at the start and then the resulting ciphertext // Create a new slice to store nonce at the start and then the resulting ciphertext
// Nonce is always 12 bytes // Nonce is always 12 bytes
combinedText := append(nonce, ciphertext...) combinedText := append(nonce, ciphertext...)
//log.Printf("EncryptSecret combined secret value is now '%x'\n", combinedText) log.Printf("EncryptSecret combined secret value is now '%x'\n", combinedText)
// Store the value back into the struct ready for database operations // Store the value back into the struct ready for database operations
s.Secret = hex.EncodeToString(combinedText) s.Secret = hex.EncodeToString(combinedText)
@@ -205,9 +211,13 @@ func (s *Secret) DecryptSecret() (*Secret, error) {
// The key argument should be the AES key, either 16 or 32 bytes // The key argument should be the AES key, either 16 or 32 bytes
// to select AES-128 or AES-256. // to select AES-128 or AES-256.
//keyString := os.Getenv("SECRETS_KEY") //keyString := os.Getenv("SECRETS_KEY")
keyString := secretKey //keyString := secretKey
key := []byte(keyString) //key := []byte(keyString)
key, err := ProvideKey()
if err != nil {
return s, err
}
if len(s.Secret) < nonceSize { if len(s.Secret) < nonceSize {
log.Printf("DecryptSecret ciphertext is too short to decrypt\n") log.Printf("DecryptSecret ciphertext is too short to decrypt\n")
@@ -220,13 +230,13 @@ func (s *Secret) DecryptSecret() (*Secret, error) {
return s, err return s, err
} }
//log.Printf("DecryptSecret processing secret '%x'\n", crypted) log.Printf("DecryptSecret processing secret '%x'\n", crypted)
// The nonce is the first 12 bytes from the ciphertext // The nonce is the first 12 bytes from the ciphertext
nonce := crypted[:nonceSize] nonce := crypted[:nonceSize]
ciphertext := crypted[nonceSize:] ciphertext := crypted[nonceSize:]
//log.Printf("DecryptSecret applying key '%v' and nonce '%x' to ciphertext '%x'\n", key, nonce, ciphertext) log.Printf("DecryptSecret applying key '%v' and nonce '%x' to ciphertext '%x'\n", key, nonce, ciphertext)
block, err := aes.NewCipher(key) block, err := aes.NewCipher(key)
if err != nil { if err != nil {
@@ -246,7 +256,7 @@ func (s *Secret) DecryptSecret() (*Secret, error) {
return s, err return s, err
} }
//log.Printf("DecryptSecret plaintext is '%s'\n", plaintext) log.Printf("DecryptSecret plaintext is '%s'\n", plaintext)
s.Secret = string(plaintext) s.Secret = string(plaintext)
return s, nil return s, nil

View File

@@ -15,7 +15,6 @@ import (
) )
var db *sqlx.DB var db *sqlx.DB
var secretKey string
const ( const (
sqlFile = "smt.db" sqlFile = "smt.db"
@@ -81,11 +80,6 @@ func ConnectDatabase() {
//defer db.Close() //defer db.Close()
} }
func LoadSecretKey(key string) {
// Store the secret key so that we can access it when encrypting/decrypting
secretKey = key
}
func DisconnectDatabase() { func DisconnectDatabase() {
log.Printf("DisconnectDatabase called") log.Printf("DisconnectDatabase called")
defer db.Close() defer db.Close()