Injecting a github secrets into the user repository

449 Views Asked by At

I'm trying to inject a github secret variable into a repository of an oauth 2.0 connected user (which means that i have its access token)

i'm using octokit to create a create a deploy key on the repository (of 4096 bytes) , then use sodium library (imposed by octokit docs) to endcrypt the secretValue with the deploy key after a lot of trials i recorded this problems: 1- if i put the length of the deploy key 32 bytes , i'm getting an error from github saying that the key is too short , 2- if i put a value of 4096 i'm getting an error that the key length is wrong (should be 32 bytes) 3- if i use another library different than sodium i get an error saying that i should use sodium and other libraries are not supported.

async function generatePublicKey():Promise<string> {
    return new Promise(
        (resolve, reject) => {
            generateKeyPair('rsa', {
                modulusLength: 4096,
                publicKeyEncoding: {
                    type: 'pkcs1',
                    format: 'pem'
                },
                privateKeyEncoding: {
                    type: 'pkcs8',
                    format: 'pem',
                }
            }, (err, publicKey, privateKey) => {
                if(err){
                    reject(err)
                }
                else{
                    const pemKey = sshpk.parseKey(publicKey, 'pem');
                    const sshRsa = pemKey.toString('ssh');
                    console.log("ssh key is : ",sshRsa);
                    resolve(sshRsa);
                    
                }
            });

          
    }
    );
  }
   async injectSecretIntoRepository(repository: string, owner: string, accessToken: string, secretName: string, secretValue: string) {
        const octokit = new Octokit({ auth: accessToken });
        console.debug("finding deploy key")
        const deployKeysResponse = await octokit.rest.repos.listDeployKeys({ owner, repo: repository });
        // Find the deploy key with write access
        let deployKey = deployKeysResponse.data.find((key) =>( key.read_only === false ));
        console.warn(`deploy key:  ${JSON.stringify( deployKeysResponse.data)}`)
        if (!deployKey) {
            console.warn(`Creating a new deploy key: `)
            try{
                const key = await generatePublicKey()
                    deployKey =( await octokit.rest.repos.createDeployKey({
                    owner,
                    repo: repository,
                    title: 'Componly deploy key',
                    key,
                    read_only: false,
                  })).data;
                  console.warn(`Deploy key created ${JSON.stringify(deployKey)}`)

            }catch(err){
                console.warn(`failed to create a new deploy key`+err)
                throw new InternalServerErrorException(err)
            }
         
        }
        
        try {
        const sodium = await SodiumPlus.auto()
       //
       //@ts-ignore


       //
        const secretKey = CryptographyKey.from(deployKey.key)
        const nonce = await sodium.randombytes_buf(sodium.CRYPTO_BOX_NONCEBYTES)
        //@ts-ignore
            const encryptedValue =  await  sodium.crypto_secretbox (secretValue,nonce,secretKey)
            // Create or update the secret with the encrypted value
            await octokit.rest.actions.createOrUpdateRepoSecret({
                owner,
                repo: repository,
                secret_name: secretName,
                encrypted_value:encryptedValue.toString("base64")

            });
    
            console.log("Secret injected successfully.");
        } catch (err) {
            console.error("Error injecting secret:", err);
            throw new InternalServerErrorException("Failed to inject secret into the repository.");
        }
    }
1

There are 1 best solutions below

1
VonC On BEST ANSWER

There is a conflict because different parts of the process require different types of cryptographic keys with different lengths, and it appears that there might have been some confusion in using the same key for different purposes.

From what I can see, you need two different keys:

  1. SSH Deploy Key:

    • Type: Asymmetric (RSA in your case).
    • Length: Typically 2048 bits or higher (4096 bits in your case).
    • Purpose: This key is used to authenticate access to the GitHub repository. The public part of the SSH key is stored in the repository's settings, and the private part is used by clients to authenticate themselves.
    • Usage in Code: In your code, you are generating an RSA key pair with a length of 4096 bits and using it as a deploy key for the repository.
  2. Encryption Key for Secrets:

    • Type: Symmetric (a single key used for both encryption and decryption).
    • Length: 256 bits (32 bytes).
    • Purpose: This key is used to encrypt the secret before storing it in the GitHub repository. The crypto_secretbox function from the sodium library is used for symmetric encryption, and it requires a 32-byte key.
    • Usage in Code: In your original code, you were trying to use the SSH deploy key (which is asymmetric and 4096 bits in length) as an encryption key. However, this is not compatible with the requirements of the crypto_secretbox function, which expects a 32-byte symmetric key.

So:

  • The SSH deploy key is asymmetric and has a length of 4096 bits, which is suitable for authenticating access to the repository.
  • The encryption key for encrypting secrets must be symmetric and 32 bytes long (256 bits) to be compatible with the crypto_secretbox function from the sodium library.

But: in your original code, it appears that the SSH deploy key was being used as the encryption key. This causes a conflict because the types and lengths of the keys are different for different purposes (SSH authentication vs. secret encryption).

To resolve this conflict, you need to generate two separate keys:

  1. Continue to generate the 4096-bit RSA key pair for use as a deploy key.
  2. Generate a separate 32-byte symmetric key specifically for encrypting the secret value with sodium's crypto_secretbox.
async function injectSecretIntoRepository(repository, owner, accessToken, secretName, secretValue) {
    const octokit = new Octokit({ auth: accessToken });
    
    // ... (Your existing code to create/find the deploy key, unchanged) ...
    
    try {
        const sodium = await SodiumPlus.auto();

        // Generate a 32-byte encryption key for encrypting the secret
        const secretKey = await sodium.randombytes_buf(32); // 32 bytes = 256 bits
        
        // Encrypt the secret value
        const nonce = await sodium.randombytes_buf(sodium.CRYPTO_BOX_NONCEBYTES);
        const encryptedValue = await sodium.crypto_secretbox(secretValue, nonce, secretKey);

        // Create or update the secret with the encrypted value
        await octokit.rest.actions.createOrUpdateRepoSecret({
            owner,
            repo: repository,
            secret_name: secretName,
            encrypted_value: encryptedValue.toString("base64")
        });

        console.log("Secret injected successfully.");
    } catch (err) {
        console.error("Error injecting secret:", err);
        throw new InternalServerErrorException("Failed to inject secret into the repository.");
    }
}

This modified version of your function separates the concerns of the deploy key (used for repository access) and the encryption key (used for encrypting the secret).
By generating a separate 32-byte key for encryption, it should resolve the issues you were facing with key length.


So if a GitHub action references the secretName, how will GitHub decrypt the stored encrypted secret if we just used a random 32 secret key?

Isn't the deploy key the one used by GitHub to decrypt the secret ?

GitHub does indeed need a way to decrypt the secret when it is used in a GitHub Action.
However, the deploy key is not used for this purpose. The deploy key is primarily for granting access to the repository and is not used for encrypting or decrypting secrets.

When you store a secret in a GitHub repository using the GitHub API, you are expected to encrypt the secret using a public key provided by GitHub. GitHub retains the corresponding private key. When a GitHub Action runs and references a secret, GitHub uses its private key to decrypt the secret.

Meaning:

  • First, fetch the public key that GitHub provides for your repository. You can fetch this key through the GitHub API (octokit.rest.actions.getRepoPublicKey).
  • Use the fetched public key to encrypt your secret. You must use asymmetric encryption here. Since you are using the sodium library, you will likely be using the crypto_box_seal function.
  • Store the encrypted secret using the GitHub API. When you store the secret, you are storing the encrypted value.
  • When you reference the secret in a GitHub Action, GitHub will automatically decrypt it using its private key.
async function injectSecretIntoRepository(repository, owner, accessToken, secretName, secretValue) {
    const octokit = new Octokit({ auth: accessToken });
    
    try {
        // Step 1: Fetch GitHub's public key
        const { data: { key, key_id } } = await octokit.rest.actions.getRepoPublicKey({
            owner,
            repo: repository
        });

        // Step 2: Encrypt the secret using GitHub's public key
        const sodium = await SodiumPlus.auto();
        const publicKey = CryptographyKey.from(Buffer.from(key, 'base64'));
        const encryptedValue = await sodium.crypto_box_seal(secretValue, publicKey);

        // Step 3: Store the encrypted secret
        await octokit.rest.actions.createOrUpdateRepoSecret({
            owner,
            repo: repository,
            secret_name: secretName,
            encrypted_value: Buffer.from(encryptedValue).toString('base64'),
            key_id: key_id
        });

        console.log("Secret injected successfully.");
    } catch (err) {
        console.error("Error injecting secret:", err);
        throw new InternalServerErrorException("Failed to inject secret into the repository.");
    }
}

In this modified version, the script first fetches the public key from GitHub, uses it to encrypt the secret, and then stores the encrypted secret.

When a GitHub Action references the secret by name, GitHub automatically decrypts it using the corresponding private key that it holds.