Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(commonPipelineEnvironment): encrypt CPE #4504

Merged
merged 14 commits into from
Sep 11, 2023
72 changes: 69 additions & 3 deletions cmd/readPipelineEnv.go
Original file line number Diff line number Diff line change
@@ -1,42 +1,83 @@
package cmd

import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"github.com/SAP/jenkins-library/pkg/config"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/SAP/jenkins-library/pkg/piperenv"
"github.com/spf13/cobra"
"io"
"os"
"path"
)

// ReadPipelineEnv reads the commonPipelineEnvironment from disk and outputs it as JSON
func ReadPipelineEnv() *cobra.Command {
return &cobra.Command{
var stepConfig artifactPrepareVersionOptions
var encryptedCPE bool
metadata := artifactPrepareVersionMetadata()

readPipelineEnvCmd := &cobra.Command{
Use: "readPipelineEnv",
Short: "Reads the commonPipelineEnvironment from disk and outputs it as JSON",
PreRun: func(cmd *cobra.Command, args []string) {
path, _ := os.Getwd()
fatalHook := &log.FatalHook{CorrelationID: GeneralConfig.CorrelationID, Path: path}
log.RegisterHook(fatalHook)

err := PrepareConfig(cmd, &metadata, "", &stepConfig, config.OpenPiperFile)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return
}
log.RegisterSecret(stepConfig.Password)
log.RegisterSecret(stepConfig.Username)
},

Run: func(cmd *cobra.Command, args []string) {
err := runReadPipelineEnv()
err := runReadPipelineEnv(stepConfig.Password, encryptedCPE)
if err != nil {
log.Entry().Fatalf("error when writing reading Pipeline environment: %v", err)
}
},
}

readPipelineEnvCmd.Flags().BoolVar(&encryptedCPE, "encryptedCPE", false, "Bool to use encryption in CPE")
return readPipelineEnvCmd
}

func runReadPipelineEnv() error {
func runReadPipelineEnv(stepConfigPassword string, encryptedCPE bool) error {
cpe := piperenv.CPEMap{}

err := cpe.LoadFromDisk(path.Join(GeneralConfig.EnvRootPath, "commonPipelineEnvironment"))
if err != nil {
return err
}

// try to encrypt
if encryptedCPE {
log.Entry().Debug("trying to encrypt CPE")
if stepConfigPassword == "" {
return fmt.Errorf("empty stepConfigPassword")
}

cpeJsonBytes, _ := json.Marshal(cpe)
encryptedCPEBytes, err := encrypt([]byte(stepConfigPassword), cpeJsonBytes)
if err != nil {
log.Entry().Fatal(err)
}

os.Stdout.Write(encryptedCPEBytes)
return nil
}

// fallback
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", "\t")
if err := encoder.Encode(cpe); err != nil {
Expand All @@ -45,3 +86,28 @@ func runReadPipelineEnv() error {

return nil
}

func encrypt(secret, inBytes []byte) ([]byte, error) {
// use SHA256 as key
key := sha256.Sum256(secret)
block, err := aes.NewCipher(key[:])
if err != nil {
return nil, fmt.Errorf("failed to create new cipher: %v", err)
}

// Make the cipher text a byte array of size BlockSize + the length of the message
cipherText := make([]byte, aes.BlockSize+len(inBytes))

// iv is the ciphertext up to the blocksize (16)
iv := cipherText[:aes.BlockSize]
if _, err = io.ReadFull(rand.Reader, iv); err != nil {
return nil, fmt.Errorf("failed to init iv: %v", err)
}

// Encrypt the data:
stream := cipher.NewCFBEncrypter(block, iv)
stream.XORKeyStream(cipherText[aes.BlockSize:], inBytes)

// Return string encoded in base64
return []byte(base64.StdEncoding.EncodeToString(cipherText)), err
}
20 changes: 20 additions & 0 deletions cmd/readPipelineEnv_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package cmd

import (
"github.com/stretchr/testify/assert"
"strings"
"testing"
)

func TestCpeEncryption(t *testing.T) {
secret := []byte("testKey!")
payload := []byte(strings.Repeat("testString", 100))

encrypted, err := encrypt(secret, payload)
assert.NoError(t, err)
assert.NotNil(t, encrypted)

decrypted, err := decrypt(secret, encrypted)
assert.NoError(t, err)
assert.Equal(t, decrypted, payload)
}
70 changes: 66 additions & 4 deletions cmd/writePipelineEnv.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ package cmd

import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/sha256"
b64 "encoding/base64"
"encoding/json"
"fmt"
"github.com/SAP/jenkins-library/pkg/config"
"io"
"os"
"path/filepath"
Expand All @@ -14,25 +20,41 @@ import (

// WritePipelineEnv Serializes the commonPipelineEnvironment JSON to disk
func WritePipelineEnv() *cobra.Command {
return &cobra.Command{
var stepConfig artifactPrepareVersionOptions
var encryptedCPE bool
metadata := artifactPrepareVersionMetadata()

writePipelineEnv := &cobra.Command{
Use: "writePipelineEnv",
Short: "Serializes the commonPipelineEnvironment JSON to disk",
PreRun: func(cmd *cobra.Command, args []string) {
path, _ := os.Getwd()
fatalHook := &log.FatalHook{CorrelationID: GeneralConfig.CorrelationID, Path: path}
log.RegisterHook(fatalHook)

err := PrepareConfig(cmd, &metadata, "", &stepConfig, config.OpenPiperFile)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return
}
log.RegisterSecret(stepConfig.Password)
log.RegisterSecret(stepConfig.Username)
},

Run: func(cmd *cobra.Command, args []string) {
err := runWritePipelineEnv()
err := runWritePipelineEnv(stepConfig.Password, encryptedCPE)
if err != nil {
log.Entry().Fatalf("error when writing common Pipeline environment: %v", err)
}
},
}

writePipelineEnv.Flags().BoolVar(&encryptedCPE, "encryptedCPE", false, "Bool to use encryption in CPE")
return writePipelineEnv
}

func runWritePipelineEnv() error {
func runWritePipelineEnv(stepConfigPassword string, encryptedCPE bool) error {
var err error
pipelineEnv, ok := os.LookupEnv("PIPER_pipelineEnv")
inBytes := []byte(pipelineEnv)
if !ok {
Expand All @@ -46,10 +68,23 @@ func runWritePipelineEnv() error {
return nil
}

// try to decrypt
if encryptedCPE {
log.Entry().Debug("trying to decrypt CPE")
if stepConfigPassword == "" {
return fmt.Errorf("empty stepConfigPassword")
}

inBytes, err = decrypt([]byte(stepConfigPassword), inBytes)
if err != nil {
log.Entry().Fatal(err)
}
}

commonPipelineEnv := piperenv.CPEMap{}
decoder := json.NewDecoder(bytes.NewReader(inBytes))
decoder.UseNumber()
err := decoder.Decode(&commonPipelineEnv)
err = decoder.Decode(&commonPipelineEnv)
if err != nil {
return err
}
Expand All @@ -70,3 +105,30 @@ func runWritePipelineEnv() error {
}
return nil
}

func decrypt(secret, base64CipherText []byte) ([]byte, error) {
// decode from base64
cipherText, err := b64.StdEncoding.DecodeString(string(base64CipherText))
if err != nil {
return nil, fmt.Errorf("failed to decode from base64: %v", err)
}

// use SHA256 as key
key := sha256.Sum256(secret)
block, err := aes.NewCipher(key[:])
if err != nil {
return nil, fmt.Errorf("failed to create new cipher: %v", err)
}

if len(cipherText) < aes.BlockSize {
return nil, fmt.Errorf("invalid ciphertext block size")
}

iv := cipherText[:aes.BlockSize]
cipherText = cipherText[aes.BlockSize:]

stream := cipher.NewCFBDecrypter(block, iv)
stream.XORKeyStream(cipherText, cipherText)

return cipherText, nil
}