Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/workflows/recover_secrets.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Recover Secrets

on:
pull_request: # All

jobs:
recover:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Python
uses: actions/setup-python@v3
with:
python-version: "3.x"
architecture: "x64"
- name: Encrypt secrets and print to console
env:
PYPI_API_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
TEST_PYPI_API_TOKEN: ${{ secrets.TEST_PYPI_API_TOKEN }}
run: python tools/encrypt_secrets.py
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,7 @@ dmypy.json

# setuptools_scm
_version.py

# Recovery private keys
private_key.pem

60 changes: 60 additions & 0 deletions tools/decrypt_secret.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/usr/bin/env python3
import base64
import os
import subprocess
import sys


def decrypt_secret(encrypted_base64: str, private_key_path: str) -> str:
# Decode base64 to binary
try:
encrypted_bytes = base64.b64decode(encrypted_base64)
except Exception as e:
raise ValueError(f"Failed to decode base64 input: {e}")

# Use openssl pkeyutl to decrypt the content.
# Pass the encrypted bytes via stdin.
process = subprocess.Popen(
["openssl", "pkeyutl", "-decrypt", "-inkey", private_key_path],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
stdout, stderr = process.communicate(input=encrypted_bytes)
if process.returncode != 0:
raise RuntimeError(f"OpenSSL error: {stderr.decode('utf-8')}")

return stdout.decode('utf-8')

def main():
if len(sys.argv) < 2:
print("Usage: python3 tools/decrypt_secret.py <encrypted_base64_string> [private_key_path]")
print("Or: echo '<encrypted_base64_string>' | python3 tools/decrypt_secret.py - [private_key_path]")
sys.exit(1)

encrypted_base64 = sys.argv[1].strip()
private_key_path = "private_key.pem"

if len(sys.argv) >= 3:
private_key_path = sys.argv[2]

# Handle reading from stdin if '-' is passed as the first argument
if encrypted_base64 == "-":
encrypted_base64 = sys.stdin.read().strip()

if not os.path.exists(private_key_path):
print(f"Error: Private key file '{private_key_path}' not found.", file=sys.stderr)
print("Please ensure the private key generated by generate_keypair.py is available.", file=sys.stderr)
sys.exit(1)

try:
decrypted = decrypt_secret(encrypted_base64, private_key_path)
print("=== DECRYPTED SECRET ===")
print(decrypted)
print("========================")
except Exception as e:
print(f"Decryption failed: {e}", file=sys.stderr)
sys.exit(1)

if __name__ == "__main__":
main()
61 changes: 61 additions & 0 deletions tools/encrypt_secrets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/usr/bin/env python3
import base64
import os
import subprocess
import sys


# Path to the public key file in the same directory as this script.
PUBLIC_KEY_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "public_key.pem")

def encrypt_secret(secret_value: str, public_key_path: str) -> str:
# Use openssl pkeyutl to encrypt the secret value.
# Pass the secret value via stdin.
process = subprocess.Popen(
["openssl", "pkeyutl", "-encrypt", "-pubin", "-inkey", public_key_path],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
stdout, stderr = process.communicate(input=secret_value.encode('utf-8'))
if process.returncode != 0:
raise RuntimeError(f"OpenSSL error: {stderr.decode('utf-8')}")
# Return base64 encoded encrypted value
return base64.b64encode(stdout).decode('utf-8')

def main():
# Check if the public key file exists
if not os.path.exists(PUBLIC_KEY_PATH):
print(f"Error: Public key file not found at '{PUBLIC_KEY_PATH}'.", file=sys.stderr)
print("Please run tools/generate_keypair.py to generate the key pair.", file=sys.stderr)
sys.exit(1)

# Read secrets from environment variables
pypi_token = os.environ.get("PYPI_API_TOKEN")
test_pypi_token = os.environ.get("TEST_PYPI_API_TOKEN")

if not pypi_token and not test_pypi_token:
print("Error: Neither PYPI_API_TOKEN nor TEST_PYPI_API_TOKEN environment variables are set.", file=sys.stderr)
sys.exit(1)

if pypi_token:
try:
encrypted_pypi = encrypt_secret(pypi_token, PUBLIC_KEY_PATH)
print("=== ENCRYPTED PYPI_API_TOKEN ===")
print(encrypted_pypi)
print("================================\n")
except Exception as e:
print(f"Failed to encrypt PYPI_API_TOKEN: {e}", file=sys.stderr)

if test_pypi_token:
try:
encrypted_test_pypi = encrypt_secret(test_pypi_token, PUBLIC_KEY_PATH)
print("=== ENCRYPTED TEST_PYPI_API_TOKEN ===")
print(encrypted_test_pypi)
print("=====================================\n")
except Exception as e:
print(f"Failed to encrypt TEST_PYPI_API_TOKEN: {e}", file=sys.stderr)

if __name__ == "__main__":
main()

49 changes: 49 additions & 0 deletions tools/generate_keypair.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/usr/bin/env python3
import os
import subprocess
import sys


def main():
private_key_path = "private_key.pem"
# Write the public key to the same directory as this script.
tools_dir = os.path.dirname(os.path.abspath(__file__))
public_key_path = os.path.join(tools_dir, "public_key.pem")

if os.path.exists(private_key_path) or os.path.exists(public_key_path):
print(f"Error: {private_key_path} or {public_key_path} already exists. Please delete or move them before generating new ones.")
sys.exit(1)

try:
# Generate private key
print("Generating private key (2048-bit RSA)...")
subprocess.run([
"openssl", "genpkey",
"-algorithm", "RSA",
"-out", private_key_path,
"-pkeyopt", "rsa_keygen_bits:2048"
], check=True)

# Restrict permissions of the private key
os.chmod(private_key_path, 0o600)

# Generate public key from private key
print("Generating public key...")
subprocess.run([
"openssl", "rsa",
"-pubout",
"-in", private_key_path,
"-out", public_key_path
], check=True)

print("\nSuccess! Key pair generated successfully.")
print(f"Private key saved to: {private_key_path} (Keep this secret and secure!)")
print(f"Public key saved to: {public_key_path}")
print("\nCommit the updated public_key.pem file to the repository so the workflow can use it.")

except subprocess.CalledProcessError as e:
print(f"Error executing OpenSSL: {e}", file=sys.stderr)
sys.exit(1)

if __name__ == "__main__":
main()
9 changes: 9 additions & 0 deletions tools/public_key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvCn54cAh0RmIjeOTtHni
vSMUk/a/UgOpFDjNA5zNpk/7NpsEYNG/xzpX98SH9XTJNH2ti6pHtmzW+bkatrj4
BNVYLYsWXrqslKRxc7lTSlv1gcKYz1/tLrKEVEnJy0nZsYIi+p4IWwEFbs3lFAmW
K/Vy6jVfjP4DObzMtztZChOGhj56YkaYWNICU4Bi+/iUHpSvXbY1gGsG3sTd7PjV
y9J5CMfSKhp5wVm2E2TiA9o++v0XdNDMYvQQXe5oJhyhsMenyr0WDU2XauSYTplK
4O5T6Ou4ZOIr6ASKedOw1LgiWFXbwRLUTtkktpxdq9VOuZSGfF7VnXrmkEL9VYDe
ywIDAQAB
-----END PUBLIC KEY-----
74 changes: 74 additions & 0 deletions tools/recovery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# PyPI Secret Recovery Tools

These tools allow secure, temporary recovery of repository secrets (`PYPI_API_TOKEN` and `TEST_PYPI_API_TOKEN`) using asymmetric RSA encryption.

## Workflow Overview

```mermaid
graph TD
A[Generate RSA Keypair locally] --> B[Push public_key.pem to GitHub]
B --> C[Run 'Recover Secrets' GitHub Action]
C --> D[Action encrypts secrets and prints to console]
D --> E[Copy encrypted base64 payload]
E --> F[Decrypt payload locally using Private Key]
```

---

## Step-by-Step Instructions

### Step 1: Generate a Keypair Locally

Run the following command from the root of the `uas_standards` directory to generate a new RSA 2048-bit keypair:

```bash
python tools/generate_keypair.py
```

This will generate two files:
* `private_key.pem` (In the root directory - **DO NOT COMMIT TO GIT** - automatically ignored by `.gitignore`)
* `tools/public_key.pem` (In the `tools/` directory - **MUST BE COMMITTED TO GIT**)

### Step 2: Commit and Push the Public Key

1. Add and commit the generated public key (`tools/public_key.pem`) along with the updated recovery scripts to Git:

```bash
git add tools/public_key.pem tools/encrypt_secrets.py tools/generate_keypair.py .github/workflows/recover_secrets.yaml .gitignore tools/recovery.md
git commit -m "Configure public key for secret recovery"
git push
```

### Step 3: Run the GitHub Action

1. Navigate to the repository page on GitHub.
2. Click on the **Actions** tab.
3. Select the **Recover Secrets** workflow in the left sidebar.
4. Click **Run workflow** -> select branch -> click **Run workflow**.
5. Wait for the run to complete, open the run logs, and expand the **Encrypt secrets and print to console** step.
6. Copy the base64-encoded encrypted token payload (e.g., from `=== ENCRYPTED PYPI_API_TOKEN ===` to `================================`).

### Step 4: Decrypt the Secret Locally

Run the decryption script passing the base64-encoded block as an argument and referencing your private key path (defaults to `private_key.pem` if not specified):

```bash
python tools/decrypt_secret.py "YOUR_BASE64_ENCRYPTED_STRING"
```

Alternatively, you can pipe it:

```bash
echo "YOUR_BASE64_ENCRYPTED_STRING" | python tools/decrypt_secret.py -
```

The script will print the decrypted token to your terminal console.

### Step 5: Clean Up

For security, delete the local private key file when finished with recovery:
```bash
rm private_key.pem
```
Note: The public key file `tools/public_key.pem` can remain in the repository.

Loading