hoto/jenkins-credentials-decryptor

View on GitHub
pkg/cryptography/credentials.go

Summary

Maintainability
A
0 mins
Test Coverage
package cryptography

import (
    "crypto/aes"
    cipherLib "crypto/cipher"
    "encoding/base64"
    "encoding/binary"
    "log"
    "regexp"
    "strings"

    "github.com/hoto/jenkins-credentials-decryptor/pkg/xml"
)

/*
  This is some next level reverse engineering.
  Kudos to http://xn--thibaud-dya.fr/jenkins_credentials.html
*/
func DecryptCredentials(credentials *[]xml.Credential, secret []byte) ([]xml.Credential, error) {
    decryptedCredentials := make([]xml.Credential, len(*credentials))
    copy(decryptedCredentials, *credentials)

    for i, credential := range *credentials {
        for key, value := range credential.Tags {
            if isBase64EncodedSecret(value) {
                encodedCipher := stripBrackets(value)
                cipher := base64Decode(encodedCipher)
                decrypted := decrypt(cipher, secret)
                decryptedCredentials[i].Tags[key] = decrypted
            }
        }

    }
    return decryptedCredentials, nil
}

/*
  New format of declaring a field to be a "base64 decoded secret" is by using {} brackets.
  Example:

    <password>{AQAAABAAAAAgPT7JbBVgyWiivobt0CJEduLyP0lB3uyTj+D5WBvVk6jyG6BQFPYGN4Z3VJN2JLDm}</password>

  Old format does not use the {} brackets.
  Instead jenkins seems to be usually suffixing the encoding with '=' sign.
  Example:

     <password>B+4pJjkJXD+pzyT9lcq8M8vF+p5YU4HmWy+MWldEdG4=</password>

  I'm not sure how to distinguish other encoded secrets from the "old days of jenkins".
  I don't want to comprehend Jenkins code from 4 years ago just to handle some edge cases.
  I can't try to decode all values because there are some phrases which
  would be false positive e.g. "root" (which is a valid base64 encoding)
*/
func isBase64EncodedSecret(text string) bool {
    if isBracketed(text) {
        encoded := textBetweenBrackets(text)
        return isBase64Encoded(encoded)
    }
    if strings.HasSuffix(text, "=") {
        return isBase64Encoded(text)
    }
    return false
}

func isBase64Encoded(text string) bool {
    _, err := base64.StdEncoding.DecodeString(text)
    if err == nil {
        return true
    }
    return false
}

func stripBrackets(text string) string {
    if isBracketed(text) {
        return textBetweenBrackets(text)
    }
    return text
}

func isBracketed(text string) bool {
    return strings.HasPrefix(text, "{") && strings.HasSuffix(text, "}")
}

func base64Decode(encoded string) []byte {
    decoded, err := base64.StdEncoding.DecodeString(encoded)
    check(err)
    return decoded
}

func textBetweenBrackets(text string) string {
    return regexp.MustCompile("{(.*?)}").FindStringSubmatch(text)[1]
}

func decrypt(cipher []byte, secret []byte) string {
    if cipher[0] == 1 { // you've gotta love jenkins
        return decryptNewFormatCredentials(cipher, secret)
    } else {
        return decryptOldFormatCredentials(cipher, secret)
    }
}

func decryptNewFormatCredentials(cipher []byte, secret []byte) string {
    ivLength := binary.BigEndian.Uint32(cipher[1:5])
    dataLength := int(binary.BigEndian.Uint32(cipher[5:9]))

    cipher = cipher[9:] // strip version, iv and data length

    iv := cipher[:ivLength]
    cipher = cipher[ivLength:] //strip iv
    block, err := aes.NewCipher(secret)
    check(err)
    mode := cipherLib.NewCBCDecrypter(block, iv)
    mode.CryptBlocks(cipher, cipher)

    // Remove PKCS padding
    n := int(cipher[dataLength-1])
    return string(cipher[:dataLength-n])
}

func decryptOldFormatCredentials(decoded []byte, secret []byte) string {
    decrypted := string(decryptAes128Ecb(decoded, secret))
    return strings.Replace(decrypted, "::::MAGIC::::", "", -1)
}

func check(err error) {
    if err != nil {
        log.Panic(err)
    }
}