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: support for direct key-value pair writing in WritePipelineEnv #5208

Merged
merged 9 commits into from
Jan 14, 2025
34 changes: 2 additions & 32 deletions cmd/readPipelineEnv.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
package cmd

import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"os"
"path"

"github.com/SAP/jenkins-library/pkg/config"
"github.com/SAP/jenkins-library/pkg/encryption"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/SAP/jenkins-library/pkg/piperenv"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -69,7 +64,7 @@ func runReadPipelineEnv(stepConfigPassword string, encryptedCPE bool) error {
}

cpeJsonBytes, _ := json.Marshal(cpe)
encryptedCPEBytes, err := encrypt([]byte(stepConfigPassword), cpeJsonBytes)
encryptedCPEBytes, err := encryption.Encrypt([]byte(stepConfigPassword), cpeJsonBytes)
if err != nil {
log.Entry().Fatal(err)
}
Expand All @@ -87,28 +82,3 @@ func runReadPipelineEnv(stepConfigPassword string, encryptedCPE bool) 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
}
5 changes: 3 additions & 2 deletions cmd/readPipelineEnv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,19 @@ import (
"strings"
"testing"

"github.com/SAP/jenkins-library/pkg/encryption"
"github.com/stretchr/testify/assert"
)

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

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

decrypted, err := decrypt(secret, encrypted)
decrypted, err := encryption.Decrypt(secret, encrypted)
assert.NoError(t, err)
assert.Equal(t, decrypted, payload)
}
127 changes: 72 additions & 55 deletions cmd/writePipelineEnv.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,28 @@ package cmd

import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/sha256"
b64 "encoding/base64"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strings"

"github.com/SAP/jenkins-library/pkg/config"

"github.com/SAP/jenkins-library/pkg/encryption"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/SAP/jenkins-library/pkg/piperenv"
"github.com/spf13/cobra"
)

// WritePipelineEnv Serializes the commonPipelineEnvironment JSON to disk
// Can be used in two modes:
// 1. JSON serialization: processes JSON input from stdin or PIPER_pipelineEnv environment variable
// 2. Direct value: writes a single key-value pair using the --value flag (format: key=value)
func WritePipelineEnv() *cobra.Command {
var stepConfig artifactPrepareVersionOptions
var encryptedCPE bool
var directValue string
metadata := artifactPrepareVersionMetadata()

writePipelineEnv := &cobra.Command{
Expand All @@ -43,6 +44,13 @@ func WritePipelineEnv() *cobra.Command {
},

Run: func(cmd *cobra.Command, args []string) {
if directValue != "" {
err := writeDirectValue(directValue)
if err != nil {
log.Entry().Fatalf("error when writing direct value: %v", err)
}
return
}
err := runWritePipelineEnv(stepConfig.Password, encryptedCPE)
if err != nil {
log.Entry().Fatalf("error when writing common Pipeline environment: %v", err)
Expand All @@ -51,85 +59,94 @@ func WritePipelineEnv() *cobra.Command {
}

writePipelineEnv.Flags().BoolVar(&encryptedCPE, "encryptedCPE", false, "Bool to use encryption in CPE")
writePipelineEnv.Flags().StringVar(&directValue, "value", "", "Key-value pair to write directly (format: key=value)")
return writePipelineEnv
}

func runWritePipelineEnv(stepConfigPassword string, encryptedCPE bool) error {
var err error
pipelineEnv, ok := os.LookupEnv("PIPER_pipelineEnv")
inBytes := []byte(pipelineEnv)
if !ok {
var err error
inBytes, err = io.ReadAll(os.Stdin)
if err != nil {
return err
}
inBytes, err := readInput()
if err != nil {
return fmt.Errorf("failed to read input: %w", err)
}
if len(inBytes) == 0 {
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)
if inBytes, err = handleEncryption(stepConfigPassword, inBytes); err != nil {
return err
}
}

commonPipelineEnv := piperenv.CPEMap{}
decoder := json.NewDecoder(bytes.NewReader(inBytes))
decoder.UseNumber()
err = decoder.Decode(&commonPipelineEnv)
commonPipelineEnv, err := parseInput(inBytes)
if err != nil {
return err
return fmt.Errorf("failed to parse input: %w", err)
}

rootPath := filepath.Join(GeneralConfig.EnvRootPath, "commonPipelineEnvironment")
err = commonPipelineEnv.WriteToDisk(rootPath)
if err != nil {
return err
if _, err := writeOutput(commonPipelineEnv); err != nil {
return fmt.Errorf("failed to write output: %w", err)
}

writtenBytes, err := json.MarshalIndent(commonPipelineEnv, "", "\t")
if err != nil {
return err
return nil
}

func readInput() ([]byte, error) {
if pipelineEnv, ok := os.LookupEnv("PIPER_pipelineEnv"); ok {
return []byte(pipelineEnv), nil
}
_, err = os.Stdout.Write(writtenBytes)
if err != nil {
return err
return io.ReadAll(os.Stdin)
}

func handleEncryption(password string, data []byte) ([]byte, error) {
if password == "" {
return nil, fmt.Errorf("encryption enabled but password is empty")
}
return nil
log.Entry().Debug("decrypting CPE data")
return encryption.Decrypt([]byte(password), data)
}

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)
func parseInput(data []byte) (piperenv.CPEMap, error) {
commonPipelineEnv := piperenv.CPEMap{}
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.UseNumber()
if err := decoder.Decode(&commonPipelineEnv); err != nil {
return nil, err
}
return commonPipelineEnv, nil
}

// use SHA256 as key
key := sha256.Sum256(secret)
block, err := aes.NewCipher(key[:])
func writeOutput(commonPipelineEnv piperenv.CPEMap) (int, error) {
rootPath := filepath.Join(GeneralConfig.EnvRootPath, "commonPipelineEnvironment")
if err := commonPipelineEnv.WriteToDisk(rootPath); err != nil {
return 0, err
}

writtenBytes, err := json.MarshalIndent(commonPipelineEnv, "", "\t")
if err != nil {
return nil, fmt.Errorf("failed to create new cipher: %v", err)
return 0, err
}
return os.Stdout.Write(writtenBytes)
}

if len(cipherText) < aes.BlockSize {
return nil, fmt.Errorf("invalid ciphertext block size")
// writeDirectValue writes a single value to a file in the commonPipelineEnvironment directory
// The key-value pair should be in the format "key=value"
// The key will be used as the file name and the value as its content
func writeDirectValue(keyValue string) error {
parts := strings.SplitN(keyValue, "=", 2)
if len(parts) != 2 {
return fmt.Errorf("invalid key-value format. Expected 'key=value', got '%s'", keyValue)
}

iv := cipherText[:aes.BlockSize]
cipherText = cipherText[aes.BlockSize:]
key := parts[0]
value := parts[1]

rootPath := filepath.Join(GeneralConfig.EnvRootPath, "commonPipelineEnvironment")
filePath := filepath.Join(rootPath, key)

stream := cipher.NewCFBDecrypter(block, iv)
stream.XORKeyStream(cipherText, cipherText)
// Ensure directory exists
if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
return fmt.Errorf("failed to create directory: %v", err)
}

return cipherText, nil
return os.WriteFile(filePath, []byte(value), 0644)
}
57 changes: 57 additions & 0 deletions pkg/encryption/encryption.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package encryption

import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"fmt"
"io"
)

// Decrypt decrypts base64-encoded data using AES-CFB
func Decrypt(secret, base64CipherText []byte) ([]byte, error) {
cipherText, err := base64.StdEncoding.DecodeString(string(base64CipherText))
if err != nil {
return nil, fmt.Errorf("failed to decode from base64: %w", err)
}

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

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

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

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

return cipherText, nil
}

// Encrypt encrypts data using AES-CFB and encodes it in base64
func Encrypt(secret, inBytes []byte) ([]byte, error) {
key := sha256.Sum256(secret)
block, err := aes.NewCipher(key[:])
if err != nil {
return nil, fmt.Errorf("failed to create cipher: %w", err)
}

cipherText := make([]byte, aes.BlockSize+len(inBytes))
iv := cipherText[:aes.BlockSize]
if _, err = io.ReadFull(rand.Reader, iv); err != nil {
return nil, fmt.Errorf("failed to init iv: %w", err)
}

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

return []byte(base64.StdEncoding.EncodeToString(cipherText)), nil
}
Loading
Loading