-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcrypto_utils.py
More file actions
147 lines (133 loc) · 6.46 KB
/
crypto_utils.py
File metadata and controls
147 lines (133 loc) · 6.46 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# Copyright 2025 Cisco Systems, Inc. and its affiliates
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
"""Some utility methods around GitGuardian formatting issues mostly"""
import base64
import re
# GitGuardian has a tendency to do a bad job with both json formatting and key formatting
# in its exported secrets.
#
# A good example are RSA keys which are often formatted like this, with an extra + immediately
# after the BEGIN and before the END.
# -----BEGIN RSA PRIVATE KEY-----+MIIJ<base64_data>g==+-----END RSA PRIVATE KEY-----
#
# The actual RSA key in the example below is this false positive:
# https://chromium.googlesource.com/external/github.com/catapult-project/catapult.git/+/12fbc0e8cc3c87599577f7148c6b6835cf85baff/third_party/google-endpoints/future/backports/test/keycert2.pem
#
# When the keys are returned from GitGuardian the +'s are aligned on 64 character boundaries like so:
# {'apikey': '-----BEGIN PRIVATE KEY-----+MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAJnsJZVrppL+W5I9+zGQrrawWwE5QJpBK9nWw17mXrZ03R1cD9BamLGivVISbPlRlAVnZBEyh1ATpsB7d+
# CUQ+WHEvALquvx4+Yw5l+fXeiYRjrLRBYZuVy8yNtXzU3iWcGObcYRkUdiXdOyP7+
# sLF2YZHRvQZpzgDBKkrraeQ81w21AgMBAAECgYBEm7n07FMHWlE+0kT0sXNsLYfy+
# YE+QKZnJw9WkaDN+zFEEPELkhZVt5BjsMraJr6v2fIEqF0gGGJPkbenffVq2B5dC+
# lWUOxvJHufMK4sM3Cp6s/gOp3LP+QkzVnvJSfAyZU6l+4PGX5pLdUsXYjPxgzjzL+
# S36tF7/2Uv1WePyLUQJBAMsPhYzUXOPRgmbhcJiqi9A9c3GO8kvSDYTCKt3VMnqz+
# HBn6MQ4VQasCD1F+7jWTI0FU/3vdw8non/Fj8hhYqZcCQQDCDRdvmZqDiZnpMqDq+
# L6ZSrLTVtMvZXZbgwForaAD9uHj51TME7+eYT7EG2YCgJTXJ4YvRJEnPNyskwdKt+
# vTSTAkEAtaaN/vyemEJ82BIGStwONNw0ILsSr5cZ9tBHzqiA/tipY+e36HRFiXhP+
# QcU9zXlxyWkDH8iz9DSAmE2jbfoqwwJANlMJ65E543cjIlitGcKLMnvtCCLcKpb7+
# xSG0XJB6Lo11OKPJ66jp0gcFTSCY1Lx2CXVd+gfJrfwI1Pp562+bhwJBAJ9IfDPU+
# R8OpO9v1SGd8x33Owm7uXOpB9d63/T70AD1QOXjKUC4eXYbt0WWfWuny/RNPRuyh+
# w7DXSfUF+kPKolU=+-----END PRIVATE KEY-----'}
def is_any_private_key(key: str) -> bool:
return bool( re.search(r"-----BEGIN\s*(?:[A-Z\s]+)?PRIVATE\s*KEY-----", key) )
def is_improperly_formatted_key(key: str) -> bool:
"""
Checks if a key has been improperly formatted by GitGuardian.
This is done by checking if the secret has a '+' or '-' character
immediately following the BEGIN PRIVATE KEY
Improperly formatted GitGuardian secret keys tend to have '+' or '-' characters included at 64 byte boundaries
"""
return bool( re.search(r"-----BEGIN\s*(?:[A-Z\s]+)?PRIVATE\s*KEY-----[+-]", key) )
def remove_private_key_delimiters(text: str) -> str:
if is_improperly_formatted_key(text):
text = re.sub(r'\+\s', '', text) # Remove all '+' characters followed by whitespace
# Note this doesn't completely fix all improperly formatted keys since some don't have the whitespace
# -----BEGIN RSA PRIVATE KEY-----+MIIJKg ... Vn/9vK/sueh379g==+-----END RSA PRIVATE KEY-----
pattern = r"-----\s*BEGIN\s*(?:[A-Z\s]+)?PRIVATE\s*KEY\s*-----|-----\s*END\s*(?:[A-Z\s]+)?PRIVATE\s*KEY\s*-----"
text = re.sub(pattern, '', text, flags=re.IGNORECASE).strip()
if text.startswith('+'):
text = text[1:]
# Remove '+' characters at 64 character boundaries
text = re.sub(r'(\S{64})\+', r'\1', text)
# Sometimes things end with a '+' before END RSA PRIVATE KEY like 9g==+-----END RSA PRIVATE KEY-----
#
# We can't simply remove a '+' at the end of the secret, since it could be part of the base64 encoding
# So the best we can appear to do is strip off any '+' characters that follow '=' since this would be
# illegal.
if text.endswith("=+"):
text = text[:-1]
# Same logic but for '-' characters which are also used in improperly formatted keys
if text.startswith('-'):
text = text[1:]
text = re.sub(r'(\S{64})-', r'\1', text)
if text.endswith("=-"):
text = text[:-1]
return text
else:
pattern = r"-----\s*BEGIN\s*(?:[A-Z\s]+)?PRIVATE\s*KEY\s*-----|-----\s*END\s*(?:[A-Z\s]+)?PRIVATE\s*KEY\s*-----"
header_stripped = re.sub(pattern, '', text, flags=re.IGNORECASE).strip()
return header_stripped
# Remove '+' characters at 64 character boundaries
#print(fixed_key)
#pgp_private_key = """-----BEGIN PGP PRIVATE KEY BLOCK-----
#Version: Some Version
#Comment: Some Comment
#
#Base64EncodedData
#MoreBase64EncodedData
#-----END PGP PRIVATE KEY BLOCK-----
#"""
def remove_pgp_private_key_armor_headers(text: str):
cleaned_key = re.sub(
r"(?m)^-----BEGIN PGP PRIVATE KEY BLOCK-----\r?\n(?:[A-Za-z0-9\-]+: .*\r?\n)*\r?\n?|^-----END PGP PRIVATE KEY BLOCK-----\r?\n?",
"",
text)
return cleaned_key
def strip_key_metadata(key: str) -> str:
"""
Strips the PEM header and footer from a key.
"""
key = remove_pgp_private_key_armor_headers(key)
key = remove_private_key_delimiters(key)
key = re.sub(r'\-', '', key, flags=re.MULTILINE) # Remove all minus signs
key = re.sub(r'\n', '', key) # Remove all newlines
key = re.sub(r'\s+', '', key) # Remove all whitespace
key = re.sub(r'\\', '', key) # Remove all backslashes
return key
def parse_base64_header(secret: str) -> tuple[str, str]:
"""
Parses a Base64 encoded secret and returns the header and body.
"""
decoded = base64.b64decode(secret).decode('utf-8')
# Split the secret into username & password
parts = decoded.split(':', 1)
if len(parts) != 2:
raise ValueError("Invalid Base64 encoded secret")
username = parts[0]
password = parts[1]
return username, password
def parse_base64_key(key: str) -> bytes:
"""
Parses a Base64 encoded key and returns the decoded bytes.
"""
key = strip_key_metadata(key)
return base64.b64decode(key)
def compare_keys(key1: str, key2: str) -> bool:
"""
Compares two Base64 encoded keys.
"""
decoded_key1 = parse_base64_key(key1)
decoded_key2 = parse_base64_key(key2)
return decoded_key1 == decoded_key2