diff --git a/README.md b/README.md index a08b821..53852fd 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ This isn't super secure, probably not even as secure as Hashicorp Vault running ## 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 3. Create systemd service definition 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 | | 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 -| 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. @@ -51,6 +51,18 @@ WantedBy=multi-user.target ``` ## 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 #### Register diff --git a/controllers/unlock.go b/controllers/unlock.go index e7a2097..00de775 100644 --- a/controllers/unlock.go +++ b/controllers/unlock.go @@ -1,6 +1,41 @@ 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) { + 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"}) } diff --git a/main.go b/main.go index 7115fbe..630f977 100644 --- a/main.go +++ b/main.go @@ -49,14 +49,17 @@ func main() { log.SetOutput(logfileWriter) 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 models.ConnectDatabase() - // let the models package know our secrets key - models.LoadSecretKey(keyString) + // Set secrets key from .env file + 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. ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) diff --git a/models/db_unlock.go b/models/db_unlock.go deleted file mode 100644 index 37d8d35..0000000 --- a/models/db_unlock.go +++ /dev/null @@ -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 -} diff --git a/models/key.go b/models/key.go new file mode 100644 index 0000000..02af83a --- /dev/null +++ b/models/key.go @@ -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") + } +} diff --git a/models/secret.go b/models/secret.go index b989e53..7efd4d4 100644 --- a/models/secret.go +++ b/models/secret.go @@ -156,42 +156,48 @@ func (s *Secret) UpdateSecret() (*Secret, error) { func (s *Secret) EncryptSecret() (*Secret, error) { //keyString := os.Getenv("SECRETS_KEY") - keyString := secretKey + //keyString := secretKey // The key argument should be the AES key, either 16 or 32 bytes // 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) - //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) if err != nil { log.Printf("EncryptSecret NewCipher error '%s'\n", 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. nonce := make([]byte, nonceSize) if _, err := io.ReadFull(rand.Reader, nonce); err != nil { log.Printf("EncryptSecret nonce generation error '%s'\n", err) return s, err } - //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 - } + log.Printf("EncryptSecret random nonce value is '%x'\n", nonce) 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 // Nonce is always 12 bytes 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 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 // to select AES-128 or AES-256. //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 { log.Printf("DecryptSecret ciphertext is too short to decrypt\n") @@ -220,13 +230,13 @@ func (s *Secret) DecryptSecret() (*Secret, error) { 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 nonce := 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) if err != nil { @@ -246,7 +256,7 @@ func (s *Secret) DecryptSecret() (*Secret, error) { return s, err } - //log.Printf("DecryptSecret plaintext is '%s'\n", plaintext) + log.Printf("DecryptSecret plaintext is '%s'\n", plaintext) s.Secret = string(plaintext) return s, nil diff --git a/models/setup.go b/models/setup.go index 068a4c8..1927745 100644 --- a/models/setup.go +++ b/models/setup.go @@ -15,7 +15,6 @@ import ( ) var db *sqlx.DB -var secretKey string const ( sqlFile = "smt.db" @@ -81,11 +80,6 @@ func ConnectDatabase() { //defer db.Close() } -func LoadSecretKey(key string) { - // Store the secret key so that we can access it when encrypting/decrypting - secretKey = key -} - func DisconnectDatabase() { log.Printf("DisconnectDatabase called") defer db.Close()