From b47d9cd59e0d202057078fd34559528155a11c1e Mon Sep 17 00:00:00 2001 From: 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 Date: Sat, 23 Nov 2024 04:48:56 +0530 Subject: [PATCH] Update 4.0 --- README.md | 29 ++- keysigner/__init__.py | 12 +- keysigner/{signer.py => apk_signer.py} | 4 +- ...store_manager.py => keystore_generator.py} | 39 +++- keysigner/keystore_info.py | 4 +- keysigner/keystore_migrator.py | 178 +++++++++++++----- keysigner/main.py | 86 ++++----- keysigner/pem_to_pkcs12.py | 156 +++++++++++++++ .../{pkcs12_converter.py => pkcs12_to_pem.py} | 4 +- keysigner/utils.py | 54 +++--- pyproject.toml | 2 +- 11 files changed, 414 insertions(+), 154 deletions(-) rename keysigner/{signer.py => apk_signer.py} (97%) rename keysigner/{keystore_manager.py => keystore_generator.py} (80%) create mode 100644 keysigner/pem_to_pkcs12.py rename keysigner/{pkcs12_converter.py => pkcs12_to_pem.py} (98%) diff --git a/README.md b/README.md index e5fd8f5..a6139de 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,17 @@ # KeySigner: Keystore Management and APK Signing Tool ## Overview -KeySigner is a command-line python tool designed to simplify keystore management and APK signing. It integrates KeyTool and ApkSigner, allowing developers to easily generate keystores (JKS, BKS, PKCS12), manage certificates, and sign APKs with custom keystores supporting multiple signature schemes (v1, v2, v3). Whether you need to create, convert, or sign, KeySigner provides a user-friendly interface for developers of all levels and efficient workflow. +KeySigner is a command-line python tool designed to simplify keystore management and APK signing. It integrates KeyTool and ApkSigner, allowing developers to easily generate and migrate keystores (JKS, BKS, PKCS12), extract certificates and private keys, and sign APKs with custom keystores, supporting multiple signature schemes (v1, v2, v3). Whether you need to create, migrate, or sign, KeySigner provides a user-friendly interface for developers of all levels and efficient workflow. --- ## Features -- **Generate Keystores**: Easily create JKS, BKS, or PKCS12 keystores. -- **Keystore Migration**: Seamlessly convert JKS or BKS keystores to PKCS12 format. -- **Certificate Extraction**: Securely extract x509 certificates and private keys in PKCS8 format from PKCS12 keystores. -- **Keystore Info Display**: Displays detailed information about the selected keystore, including certificate fingerprints, public key algorithm, expiries and more. +- **Generate Keystores**: Easily generate JKS, BKS, and PKCS12 keystores. +- **Keystore Migration**: Seamlessly migrate JKS, BKS and PKCS12 to each other. +- **Export PKCS12 to PEM**: Securely extract x509 certificates and private keys in PKCS8 format from PKCS12 keystores. +- **Import PEM to PKCS12**: Import the certificate and private key as new entries in the PKCS12 keystore. +- **Keystore Info Display**: Displays detailed information about the selected keystore, including certificate fingerprints, public key algorithm, expiries and more. If you want a detailed analysis of your keystores we recommend our other tool SigTool. [Check out SigTool...](https://github.com/muhammadrizwan87/sigtool) - **APK Signing**: sign APK files with custom keystores, supporting multiple signing formats (v1, v2, v3). --- @@ -87,7 +88,7 @@ To build KeySigner from source: ```bash python -m build - pip install --force-reinstall dist/keysigner-3.0-py3-none-any.whl + pip install --force-reinstall dist/keysigner-4.0-py3-none-any.whl ``` --- @@ -102,15 +103,13 @@ Example: $ keysigner Select an option: -1. Create new JKS keystore -2. Create new BKS keystore -3. Create new PKCS12 keystore -4. Migrate JKS to PKCS12 -5. Migrate BKS to PKCS12 -6. Convert PKCS12 to PEM and extract x509 certificate and private key -7. Show keystore information -8. Sign APK -9. Show Notes +1. Generate new keystore (JKS/BKS/PKCS12) +2. Migrate keystores to each other (JKS/BKS/PKCS12) +3. Convert PKCS12 to PEM and extract certificate and key +4. Convert PEM to PKCS12 +5. Show keystore information +6. Sign APK +7. Show Notes q. Quit ``` diff --git a/keysigner/__init__.py b/keysigner/__init__.py index fecc29e..e9b1944 100644 --- a/keysigner/__init__.py +++ b/keysigner/__init__.py @@ -1,14 +1,16 @@ -from .keystore_manager import KeystoreManager +from .keystore_generator import KeystoreGenerator from .keystore_migrator import KeystoreMigrator -from .pkcs12_converter import PKCS12Converter +from .pkcs12_to_pem import PKCS12ToPEM +from .pem_to_pkcs12 import PEMToPKCS12 from .keystore_info import KeystoreInfo -from .signer import APKSigner +from .apk_signer import APKSigner from .utils import * __all__ = [ - "KeystoreManager", + "KeystoreGenerator", "KeystoreMigrator", - "PKCS12Converter", + "PKCS12ToPEM", + "PEMToPKCS12", "KeystoreInfo", "APKSigner", "color_text", diff --git a/keysigner/signer.py b/keysigner/apk_signer.py similarity index 97% rename from keysigner/signer.py rename to keysigner/apk_signer.py index 1e4b929..0a7c4ba 100644 --- a/keysigner/signer.py +++ b/keysigner/apk_signer.py @@ -69,7 +69,7 @@ def sign_with_keystores(self): def sign_with_keystore(self, keystore_type): keystore_path = validate_input(cyan_text(f"Enter {keystore_type.upper()} keystore path: "), path=True) - store_pass = validate_input(cyan_text("Enter keystore password: "), password_ck=True) + store_pass = validate_input(cyan_text("Enter keystore password: "), password=True, min_length=6) alias = validate_input(cyan_text("Enter alias name: ")) base_name = os.path.splitext(os.path.basename(self.apk_file))[0] signed_apk = os.path.join(self.output_path, f"{base_name}_signed.apk") @@ -87,7 +87,7 @@ def sign_with_keystore(self, keystore_type): ] if keystore_type.lower() == 'jks': - key_pass = validate_input(cyan_text("Enter alias password: "), password_ck=True) + key_pass = validate_input(cyan_text("Enter alias password (default: same as keystore password): "), pass_opt=store_pass, min_length=6) cmd.extend(['--key-pass', f'pass:{key_pass}']) cmd.extend([self.apk_file]) diff --git a/keysigner/keystore_manager.py b/keysigner/keystore_generator.py similarity index 80% rename from keysigner/keystore_manager.py rename to keysigner/keystore_generator.py index 2c962ce..f3558ff 100644 --- a/keysigner/keystore_manager.py +++ b/keysigner/keystore_generator.py @@ -4,9 +4,9 @@ import subprocess from .utils import * -class KeystoreManager: - def __init__(self, store_type): - self.store_type = store_type +class KeystoreGenerator: + def __init__(self): + self.store_type = None self.store_name = None self.store_pass = None self.alias = None @@ -20,13 +20,20 @@ def __init__(self, store_type): def set_keystore_details(self): print_blue("\n--- Setting Keystore Details ---") + while True: + keystore_type = validate_input(cyan_text("Enter new keystore type (JKS/BKS/PKCS12): ")).upper() + if keystore_type in ['JKS', 'BKS', 'PKCS12']: + self.store_type = keystore_type + break + else: + print_red("Invalid keystore type. Please try again.") self.store_name = validate_input(cyan_text("Enter keystore name: ")) - self.store_pass = validate_input(cyan_text("Enter keystore password: "), password=True) + self.store_pass = validate_input(cyan_text("Enter keystore password: "), password=True, min_length=8) self.alias = validate_input(cyan_text("Enter alias name: ")) if self.store_type == 'PKCS12': self.key_pass = self.store_pass else: - self.key_pass = validate_input(cyan_text("Enter alias password: "), password=True) + self.key_pass = validate_input(cyan_text("Enter alias password (default: same as keystore password): "), pass_opt=self.store_pass, min_length=8) self.validity = validate_input(cyan_text("Enter validity (days, default 36500): "), required=False) or '36500' self.dname = self.generate_dname() if self.store_type == 'BKS': @@ -54,7 +61,19 @@ def generate_dname(self): l = validate_input(cyan_text("Enter L (Locality): "), required=False) st = validate_input(cyan_text("Enter ST (State): "), required=False) c = validate_input(cyan_text("Enter C (Country Code): "), required=False) - return f"CN={cn}, OU={ou}, O={o}, L={l}, ST={st}, C={c}" + + if not any([cn, ou, o, l, st, c]): + return "CN=Unknown" + + dname_parts = [] + if cn: dname_parts.append(f"CN={cn}") + if ou: dname_parts.append(f"OU={ou}") + if o: dname_parts.append(f"O={o}") + if l: dname_parts.append(f"L={l}") + if st: dname_parts.append(f"ST={st}") + if c: dname_parts.append(f"C={c}") + + return ", ".join(dname_parts) def generate_keytool_command(self): cmd = [ @@ -77,7 +96,7 @@ def generate_keytool_command(self): cmd.extend(['-providerclass', self.provider_class, '-providerpath', self.provider_path]) return cmd - def create_keystore(self): + def generate_keystore(self): try: self.set_keystore_details() self.cmd = self.generate_keytool_command() @@ -85,12 +104,12 @@ def create_keystore(self): result = subprocess.run(self.cmd) if result.returncode != 0: - print_red("Keystore creation failed.") + print_red("Keystore generation failed.") return print_green("KeyTool command executed successfully!") - print_green(f"Keystore created at: {self.store_path}") - self.handle_and_generate_command(self.cmd, "Keystore Command") + print_green(f"Keystore {self.store_type} generated at: {self.store_path}") + self.handle_and_generate_command(self.cmd, f"Keystore command to generate new {self.store_type}") if self.store_type != 'BKS': self.generate_apksigner_command(self.store_path, self.store_pass, self.alias, self.key_pass) diff --git a/keysigner/keystore_info.py b/keysigner/keystore_info.py index 44991de..7651a6e 100644 --- a/keysigner/keystore_info.py +++ b/keysigner/keystore_info.py @@ -13,7 +13,7 @@ def __init__(self): def get_keystore_info(self): print_blue("\n--- Gathering Keystore Information ---") self.keystore_path = validate_input(cyan_text("Enter keystore path: "), path=True) - self.store_pass = validate_input(cyan_text("Enter keystore password: "), password_ck=True) + self.store_pass = validate_input(cyan_text("Enter keystore password: "), password=True, min_length=6) self.determine_keystore_type() def determine_keystore_type(self): @@ -26,7 +26,7 @@ def determine_keystore_type(self): self.keystore_type = 'PKCS12' else: while True: - keystore_type = validate_input(cyan_text("Keystore type not detected. Please enter keystore type (JKS/BKS/PKCS12): ")).upper() + keystore_type = validate_input(cyan_text("Please enter keystore type (JKS/BKS/PKCS12): ")).upper() if keystore_type in ['JKS', 'BKS', 'PKCS12']: self.keystore_type = keystore_type break diff --git a/keysigner/keystore_migrator.py b/keysigner/keystore_migrator.py index 5761307..fe05b61 100644 --- a/keysigner/keystore_migrator.py +++ b/keysigner/keystore_migrator.py @@ -2,76 +2,158 @@ import os import subprocess -from getpass import getpass -from .keystore_manager import KeystoreManager from .utils import * -class KeystoreMigrator(KeystoreManager): - def __init__(self, store_type): - super().__init__(store_type) +class KeystoreMigrator: + def __init__(self): + self.src_path = None + self.src_store_type = None + self.src_store_pass = None + self.src_alias = None + self.src_key_pass = None + self.dest_store_name = None + self.dest_store_type = None + self.dest_store_pass = None + self.dest_alias = None + self.dest_key_pass = None + self.output_path = None + self.dest_path = None + self.provider_class = None + self.provider_path = None def get_migration_input(self): - print_blue(f"\n--- Migrating {self.store_type} Keystore ---") - self.src_path = validate_input(cyan_text(f"Enter {self.store_type} keystore path: "), path=True) - self.src_store_pass = validate_input(cyan_text("Enter source keystore password: "), password_ck=True) + print_blue("\n--- JKS/BKS/PKCS12 Keystore Migration ---") + self.src_path = validate_input(cyan_text("Enter source keystore path: "), path=True) + + file_extension = os.path.splitext(self.src_path)[1].lower() + if file_extension == '.jks': + self.src_store_type = 'JKS' + elif file_extension == '.bks': + self.src_store_type = 'BKS' + elif file_extension == '.p12': + self.src_store_type = 'PKCS12' + else: + while True: + keystore_type = validate_input(cyan_text("Enter source keystore type (JKS/BKS/PKCS12): ")).upper() + if keystore_type in ['JKS', 'BKS', 'PKCS12']: + self.src_store_type = keystore_type + break + else: + print_red("Invalid source keystore type. Please try again.") + + self.src_store_pass = validate_input(cyan_text("Enter source keystore password: "), password=True, min_length=6) self.src_alias = validate_input(cyan_text("Enter source alias name: ")) - self.src_key_pass = validate_input(cyan_text("Enter source alias password: "), password_ck=True) - self.dest_alias = validate_input(cyan_text("Enter destination alias name (default: same as source alias name): "), required=False) or self.src_alias - self.dest_store_pass = getpass(cyan_text("Enter destination keystore password (default: same as source keystore password): ")) or self.src_store_pass - if len(self.dest_store_pass) < 6: - print_red(f"Password must be at least 6 characters long.") - self.dest_store_pass = validate_input(cyan_text("Enter destination keystore password: "), password_ck=True) + if self.src_store_type != 'PKCS12': + self.src_key_pass = validate_input(cyan_text("Enter source alias password (default: same as source keystore password): "), pass_opt=self.src_store_pass, min_length=6) + else: + self.src_key_pass = self.src_store_pass + self.dest_store_name = os.path.splitext(os.path.basename(self.src_path))[0] + + valid_dest_types = {'JKS', 'BKS', 'PKCS12'} - {self.src_store_type.upper()} + while True: + keystore_type = validate_input(cyan_text(f"Enter destination keystore type ({'/'.join(valid_dest_types)}): "), required=True).upper() + if keystore_type in valid_dest_types: + self.dest_store_type = keystore_type + break + else: + print_red("Invalid destination keystore type. Please try again.") + + self.dest_store_pass = validate_input(cyan_text("Enter destination keystore password (default: same as source keystore password): "), pass_opt=self.src_store_pass, min_length=6) + self.dest_alias = validate_input(cyan_text("Enter destination alias name (default: same as source alias name): "), required=False) or self.src_alias + + if self.dest_store_type != 'PKCS12': + self.dest_key_pass = validate_input(cyan_text("Enter destination alias password (default: same as source alias password): "), pass_opt=self.src_key_pass, min_length=6) + else: + self.dest_key_pass = self.dest_store_pass + self.output_path = validate_input(cyan_text(f"Enter output path (default: {os.path.abspath('keystore')}): "), required=False) - if not self.output_path or not os.path.exists(self.output_path): self.output_path = ensure_directory(self.output_path) else: self.output_path = ensure_directory(self.output_path, dir_name=os.path.basename(self.output_path)) - self.dest_path = os.path.join(self.output_path, f"{os.path.basename(self.src_path).replace(f'.{self.store_type.lower()}', '')}.p12") - self.store_name = os.path.join(self.output_path, f"{os.path.splitext(os.path.basename(self.src_path))[0]}") + store_type = 'p12' if self.dest_store_type.lower() == 'pkcs12' else self.dest_store_type.lower() + self.dest_path = os.path.join(self.output_path, f"{os.path.splitext(os.path.basename(self.src_path))[0]}.{store_type}") + + if self.src_store_type == 'BKS' or self.dest_store_type == "BKS": + self.provider_class = "org.bouncycastle.jce.provider.BouncyCastleProvider" + root_dir = os.path.dirname(__file__) + self.provider_path = os.path.join(root_dir, 'lib', 'bcprov-jdk18on-1.78.jar') print_green("Migration input successfully gathered!") + def generate_migration_command(self): + cmd = [ + 'keytool', '-importkeystore', + '-srckeystore', self.src_path, + '-srcstoretype', self.src_store_type, + '-srcstorepass', self.src_store_pass, + '-srcalias', self.src_alias, + '-srckeypass', self.src_key_pass, + '-destkeystore', self.dest_path, + '-deststoretype', self.dest_store_type, + '-deststorepass', self.dest_store_pass, + '-destalias', self.dest_alias, + '-destkeypass', self.dest_key_pass + ] + + if self.src_store_type == 'BKS' or self.dest_store_type == 'BKS': + cmd.extend(['-providerclass', self.provider_class, '-providerpath', self.provider_path]) + + return cmd + + def generate_apksigner_command(self): + print_blue("\n--- Generating APK Signer Command ---") + cmd = [ + "apksigner", "sign", + "--ks", self.dest_path, + "--ks-pass", f"pass:{self.dest_store_pass}", + "--ks-key-alias", self.dest_alias, + "--key-pass", f"pass:{self.dest_key_pass}", + "--v1-signing-enabled", "true", + "--v2-signing-enabled", "true", + "--v3-signing-enabled", "true", + "--v4-signing-enabled", "false", + "--out", "signed.apk", "unsigned.apk" + ] + + self.handle_and_generate_command(cmd, "APK Signer Command") + def migrate_keystore(self): try: self.get_migration_input() - - self.cmd = [ - 'keytool', '-importkeystore', - '-srckeystore', self.src_path, - '-srcstoretype', self.store_type, - '-srcstorepass', self.src_store_pass, - '-srcalias', self.src_alias, - '-srckeypass', self.src_key_pass, - '-destkeystore', self.dest_path, - '-deststoretype', 'PKCS12', - '-deststorepass', self.dest_store_pass, - '-destalias', self.dest_alias, - '-destkeypass', self.dest_store_pass - ] - - if self.store_type == 'BKS': - self.provider_class = 'org.bouncycastle.jce.provider.BouncyCastleProvider' - root_dir = os.path.dirname(__file__) - self.provider_path = os.path.join(root_dir, 'lib', 'bcprov-jdk18on-1.78.jar') - self.cmd.extend(['-providerclass', self.provider_class, '-providerpath', self.provider_path]) - - print_blue("\n--- Executing Keystore Command ---") + self.cmd = self.generate_migration_command() + print_blue("\n--- Executing Keystore Migration Command ---") result = subprocess.run(self.cmd) - + if result.returncode != 0: print_red("Keystore migration failed.") return - - print_green("KeyTool command executed successfully!") - print_green(f"Keystore migrated to PKCS12 at: {self.dest_path}") - - self.store_type = "PKCS12" - self.handle_and_generate_command(self.cmd, "Keystore Command") - self.generate_apksigner_command(self.dest_path, self.dest_store_pass, self.dest_alias, self.dest_store_pass) + + print_green("Keystore migration executed successfully!") + print_green(f"{self.src_store_type} migrated to {self.dest_store_type} at: {self.dest_path}") + self.handle_and_generate_command(self.cmd, f"{self.src_store_type} to {self.dest_store_type} Keystore Migration Command") + if self.dest_store_type != 'BKS': + self.generate_apksigner_command() except Exception as e: print_red(f"Error occurred: {e}") - exit() \ No newline at end of file + exit() + + def handle_and_generate_command(self, cmd_list, description): + print_blue(f"\n--- {description} ---") + full_command = [] + for cmd in cmd_list: + if ' ' in cmd: + full_command.append(f'"{cmd}"') + else: + full_command.append(cmd) + full_command = " ".join(full_command) + + cmd_file = os.path.abspath(os.path.join(self.output_path, f"{self.dest_store_name}_{self.dest_store_type}_commands.txt")) + with open(cmd_file, 'a') as f: + f.write(f"{description}:\n") + f.write(full_command) + f.write("\n\n") + print_green(f"{description} exported to {cmd_file}") \ No newline at end of file diff --git a/keysigner/main.py b/keysigner/main.py index c512018..13e3758 100644 --- a/keysigner/main.py +++ b/keysigner/main.py @@ -2,26 +2,30 @@ # -*- coding: utf-8 -*- # import readline -from .keystore_manager import KeystoreManager +from .keystore_generator import KeystoreGenerator from .keystore_migrator import KeystoreMigrator -from .pkcs12_converter import PKCS12Converter +from .pkcs12_to_pem import PKCS12ToPEM +from .pem_to_pkcs12 import PEMToPKCS12 from .keystore_info import KeystoreInfo -from .signer import APKSigner +from .apk_signer import APKSigner from .utils import * def show_notes(): - print(color_text("1. If you want to add multiple entries to the existing keystore, just keep the 'keystore name', 'keystore password', and 'output path' the same, and tweak the other details for each new entry.", 36)) - print(color_text("\n2. During migration from JKS or BKS to PKCS12, there are opportunity to adjust the source information. So, feel free to change the 'keystore password' and 'alias name'. Don’t worry, your certificate and key will stay unchanged. You can check integrity of your certificate using option seven.", 36)) - print(color_text("\n3. PKCS12 format doesn’t deal with different passwords for the keystore and aliases. So, the alias password will automatically be set to the keystore password.", 36)) - print(color_text("\n4. Before you convert from PKCS12 to PEM, remember that 'x509.pem' and '.pk8' files will only come from the first entry. If you’ve got more entries, you’ll need to handle those one by one.", 36)) - print(color_text("\n5. Each time you create a keystore, make sure to generate its '.x509.pem' and '.pk8' files too. This way, you don’t have to keep re-entering passwords and names when you’re signing your APK.", 36)) - print(color_text("\n6. Direct signing of an APK file using apksigner with a BKS-type keystore is not supported. Therefore, it is necessary to first migrate from BKS to PKCS12 or convert from BKS to PEM.", 36)) - print(color_text("\n7. If you sign your APK using apksigner and make further changes to the APK, the APK's signature is invalidated. If you use zipalign to align your APK, use it before signing the APK.", 36)) - print(color_text("\n8. Backup all generated keystores and their details. Losing them could result in significant damage.", 36)) - print(color_text("\n9. Do not trust untrusted sources or servers for saving such confidential information.", 36)) - print(color_text("\n10. If you want to perform a detailed analysis of your keystore to check integrity, we recommend using our other tool, Sigtool. This tool provides a deeper analysis of signatures. Currently, version 1.0 analyzes information from APK files, while the upcoming update, version 2.0, will expand its capabilities to include direct certificate analysis. You can find Sigtool here:", 36)) + print(color_text("1. '.keystore' is just an extension name that is actually another form of the '.jks' extension. So, if you have a '.keystore' extension, you should consider it as '.jks'.", 36)) + print(color_text("\n2. Our suggestion is that if you need to generate a new keystore, generate it in PKCS12. Or, if you have already generated a JKS, migrate it to PKCS12. This is because PKCS12 is modern, secure, and widely compatible. We do not recommend migrating from PKCS12 to JKS at all. This option is provided because some third-party tools do not consider users' independence and force them to use a specific keystore.", 36)) + print(color_text("\n3. If you want to add multiple entries to the existing keystore, just keep the 'keystore name', 'keystore password', and 'output path' the same, and tweak the other details for each new entry.", 36)) + print(color_text("\n4. During migration from keystores to each other, there are opportunity to adjust the source information. So, feel free to change the 'keystore password' and 'alias name'. Don’t worry, your certificate and key will stay unchanged. You can check integrity of your certificate using option five.", 36)) + print(color_text("\n5. PKCS12 format doesn’t deal with different passwords for the keystore and aliases. So, the alias password will automatically be set to the keystore password.", 36)) + print(color_text("\n6. The default workflow of keytool is to access the first entry of the source keystore and match the alias key password with the keystore password.", 36)) + print(color_text("\n7. Before you convert from PKCS12 to PEM, remember that 'x509.pem' and '.pk8' files will only come from the first entry. If you’ve got more entries, you’ll need to handle those one by one.", 36)) + print(color_text("\n8. Each time you create a keystore, make sure to generate its '.x509.pem' and '.pk8' files too. This way, you don’t have to keep re-entering passwords and names when you’re signing your APK.", 36)) + print(color_text("\n9. Direct signing of an APK file using apksigner with a BKS-type keystore is not supported. Therefore, it is necessary to first migrate from BKS to other keystores.", 36)) + print(color_text("\n10. If you sign your APK using apksigner and make further changes to the APK, the APK's signature is invalidated. If you use zipalign to align your APK, use it before signing the APK.", 36)) + print(color_text("\n11. Backup all generated keystores and their details. Losing them could result in significant damage.", 36)) + print(color_text("\n12. Do not trust untrusted sources or servers for saving such confidential information.", 36)) + print(color_text("\n13. If you want to add your keystore verification to your project, you should check out our other tool, SigTool. SigTool will generate all the details of your keystore for you, such as SHA-1, SHA-224, SHA-256, SHA-356, SHA-512, MD5, CRC32, Java-style HashCode, and their base64 encoded results. It will also generate the smali byte array format of your keystore. You can find Sigtool here:", 36)) print("https://github.com/muhammadrizwan87/sigtool") - print(color_text("\n11. Documentations:", 36)) + print(color_text("\n14. Documentations:", 36)) print(color_text("Keystore:", 36)) print("https://docs.oracle.com/en/java/javase/21/docs/specs/man/keytool.html") print(color_text("Apksigner:", 36)) @@ -32,52 +36,42 @@ def main(): meta_data() while True: print_magenta("\nSelect an option:") - print_yellow("1. Create new JKS keystore") - print_yellow("2. Create new BKS keystore") - print_yellow("3. Create new PKCS12 keystore") - print_yellow("4. Migrate JKS to PKCS12") - print_yellow("5. Migrate BKS to PKCS12") - print_yellow("6. Convert PKCS12 to PEM and extract x509 certificate and private key") - print_yellow("7. Show keystore information") - print_yellow("8. Sign APK") - print_yellow("9. Show Notes") + print_yellow("1. Generate new keystore (JKS/BKS/PKCS12)") + print_yellow("2. Migrate keystores to each other (JKS/BKS/PKCS12)") + print_yellow("3. Convert PKCS12 to PEM and extract certificate and key") + print_yellow("4. Convert PEM to PKCS12") + print_yellow("5. Show keystore information") + print_yellow("6. Sign APK") + print_yellow("7. Show Notes") print_blue("q. Quit") - choice = input(cyan_text("\nEnter choice (1-9 or q to quit): ")).lower() + choice = input(cyan_text("\nEnter choice (1-7 or q to quit): ")).lower() if choice == '1': - print_blue("\nCreating new JKS keystore...") - keystore_manager = KeystoreManager('JKS') - keystore_manager.create_keystore() + print_blue("\nGenerating new keystore (JKS/BKS/PKCS12)...") + generator = KeystoreGenerator() + generator.generate_keystore() elif choice == '2': - print_blue("\nCreating new BKS keystore...") - keystore_manager = KeystoreManager('BKS') - keystore_manager.create_keystore() - elif choice == '3': - print_blue("\nCreating new PKCS12 keystore...") - keystore_manager = KeystoreManager('PKCS12') - keystore_manager.create_keystore() - elif choice == '4': - print_blue("\nMigrating JKS to PKCS12...") - migrator = KeystoreMigrator('JKS') - migrator.migrate_keystore() - elif choice == '5': - print_blue("\nMigrating BKS to PKCS12...") - migrator = KeystoreMigrator('BKS') + print_blue("\nMigrating keystores (JKS/BKS/PKCS12)...") + migrator = KeystoreMigrator() migrator.migrate_keystore() - elif choice == '6': + elif choice == '3': print_blue("\nConverting PKCS12 to PEM and extracting x509 and private key...") - converter = PKCS12Converter() + converter = PKCS12ToPEM() converter.convert_p12_to_pem() - elif choice == '7': + elif choice == '4': + print_blue("\nConverting PEN to PKCS12...") + converter = PEMToPKCS12() + converter.convert_pem_to_p12() + elif choice == '5': print_blue("\nShowing keystore information...") keystore_info = KeystoreInfo() keystore_info.show_keystore_info() - elif choice == '8': + elif choice == '6': print_blue("\nSigning APK...") signer = APKSigner() signer.sign_with_keystores() - elif choice == '9': + elif choice == '7': print_blue("\nShowing Notes...") show_notes() elif choice in ['q', 'x']: diff --git a/keysigner/pem_to_pkcs12.py b/keysigner/pem_to_pkcs12.py new file mode 100644 index 0000000..ae73992 --- /dev/null +++ b/keysigner/pem_to_pkcs12.py @@ -0,0 +1,156 @@ +# -*- coding: utf-8 -*- + +import os +import subprocess +from .utils import * + +class PEMToPKCS12: + def __init__(self): + self.cert_path = None + self.key_path = None + self.pem_key_path = None + self.store_name = None + self.alias = None + self.store_pass = None + self.output_path = None + self.keystore_path = None + + def get_conversion_input(self): + print_blue("\n--- Gathering PEM to PKCS12 Conversion Input ---") + self.cert_path = validate_input(cyan_text("Enter x509 certificate path: "), path=True) + self.key_path = validate_input(cyan_text("Enter private key path (PKCS8 format - .pk8): "), path=True) + self.store_name = validate_input(cyan_text("Enter new keystore name: ")) + self.alias = validate_input(cyan_text("Enter alias for the key: ")) + self.store_pass = validate_input(cyan_text("Enter keystore password: "), password=True, min_length=8) + self.output_path = validate_input(cyan_text(f"Enter output path (default: {os.path.abspath('keystore')}): "), required=False) + + if not self.output_path or not os.path.exists(self.output_path): + self.output_path = ensure_directory(self.output_path) + else: + self.output_path = ensure_directory(self.output_path, dir_name=os.path.basename(self.output_path)) + + self.keystore_path = os.path.join(self.output_path, f"{self.store_name}.p12") + base_filename = os.path.splitext(os.path.basename(self.key_path))[0] + self.pem_key_path = os.path.join(self.output_path, f"{base_filename}_key.pem") + print_green("Conversion input successfully gathered!") + + def convert_pk8_to_pem(self): + try: + self.get_conversion_input() + print_blue("\n--- Converting PKCS8 (PK8) Key to PEM Format ---") + pk8_to_pem_cmd = [ + "openssl", "pkcs8", + "-inform", "DER", + "-outform", "PEM", + "-nocrypt", + "-in", self.key_path, + "-out", self.pem_key_path + ] + + result = subprocess.run(pk8_to_pem_cmd) + if result.returncode != 0: + print_red("Private key conversion to PEM format failed.") + exit() + + print_green("Private key successfully converted to PEM format!") + print_green(f"Private key exported to: {self.pem_key_path}") + self.handle_and_generate_command([pk8_to_pem_cmd], "OpenSSL Command for PK8 to PEM Conversion") + except Exception as e: + print_red(f"Error occurred: {e}") + exit() + + def convert_pem_to_p12(self): + try: + self.convert_pk8_to_pem() + + print_blue("\n--- Converting PEM Certificate and Key to PKCS12 Keystore ---") + self.p12_cmd = [ + "openssl", "pkcs12", + "-export", + "-in", self.cert_path, + "-inkey", self.pem_key_path, + "-name", self.alias, + "-out", self.keystore_path, + "-password", f"pass:{self.store_pass}" + ] + + self.execute_command() + self.generate_apksigner_command() + except Exception as e: + print_red(f"Error occurred: {e}") + exit() + + def execute_command(self): + try: + result = subprocess.run(self.p12_cmd) + if result.returncode != 0: + print_red("PKCS12 conversion failed.") + return + + print_green("OpenSSL command executed successfully!") + if os.path.exists(self.keystore_path): + print_green(f"Keystore PKCS12 exported to: {self.keystore_path}") + self.handle_and_generate_command([self.p12_cmd], "OpenSSL Command for PEM to PKCS12 Conversion") + else: + print_red("Error: Keystore file missing after conversion.") + except Exception as e: + print_red(f"Error occurred: {e}") + exit() + + def generate_apksigner_command(self): + try: + print_blue("\n--- Generating APKSigner Command ---") + self.apksigner_cmd = [ + "apksigner", "sign", + "--ks", self.keystore_path, + "--ks-pass", f"pass:{self.store_pass}", + "--ks-key-alias", self.alias, + "--key-pass", f"pass:{self.store_pass}", + "--v1-signing-enabled", "true", + "--v2-signing-enabled", "true", + "--v3-signing-enabled", "true", + "--v4-signing-enabled", "false", + "--out", "signed.apk", "unsigned.apk" + ] + + print_green("APKSigner command generated successfully!") + self.handle_and_generate_command([self.apksigner_cmd], "APKSigner Command") + except Exception as e: + print_red(f"Error occurred: {e}") + exit() + + def handle_and_generate_command(self, cmd_list, description): + print_blue(f"\n--- {description} ---") + if any(isinstance(i, list) for i in cmd_list): + cmd_list = [item for sublist in cmd_list for item in sublist] + + def format_cmd(cmd): + full_cmd = [] + for c in cmd: + if ' ' in c: + full_cmd.append(f'"{c}"') + else: + full_cmd.append(c) + return " ".join(full_cmd) + + commands = [] + + if "PK8 to PEM" in description: + commands.append("OpenSSL command to convert PKCS8 (PK8) Key to PEM:") + commands.append(format_cmd(cmd_list)) + elif "PEM to PKCS12" in description: + commands.append("OpenSSL command to convert PEM to PKCS12:") + commands.append(format_cmd(self.p12_cmd)) + elif "APKSigner" in description: + commands.append("APKSigner command to sign APK:") + commands.append(format_cmd(self.apksigner_cmd)) + else: + commands.append(f"{description}:") + commands.append(format_cmd(cmd_list)) + + full_command_output = "\n".join(commands) + + cmd_file = os.path.abspath(os.path.join(self.output_path, f"{self.store_name}_PKCS12_commands.txt")) + with open(cmd_file, 'a') as f: + f.write(f"{full_command_output}\n\n") + print_green(f"{description} exported to {cmd_file}") \ No newline at end of file diff --git a/keysigner/pkcs12_converter.py b/keysigner/pkcs12_to_pem.py similarity index 98% rename from keysigner/pkcs12_converter.py rename to keysigner/pkcs12_to_pem.py index 5e130c1..ac6599f 100644 --- a/keysigner/pkcs12_converter.py +++ b/keysigner/pkcs12_to_pem.py @@ -4,7 +4,7 @@ import subprocess from .utils import * -class PKCS12Converter: +class PKCS12ToPEM: def __init__(self): self.p12_path = None self.store_pass = None @@ -13,7 +13,7 @@ def __init__(self): def get_conversion_input(self): print_blue("\n--- Gathering PKCS12 Conversion Input ---") self.p12_path = validate_input(cyan_text("Enter PKCS12 keystore path: "), path=True) - self.store_pass = validate_input(cyan_text("Enter keystore password: "), password=True) + self.store_pass = validate_input(cyan_text("Enter keystore password: "), password=True, min_length=6) self.output_path = validate_input(cyan_text(f"Enter output path (default: {os.path.abspath('keystore')}): "), required=False) if not self.output_path or not os.path.exists(self.output_path): diff --git a/keysigner/utils.py b/keysigner/utils.py index 852558e..a2a49c4 100644 --- a/keysigner/utils.py +++ b/keysigner/utils.py @@ -36,7 +36,7 @@ def print_yellow(message): def logo_ascii_art(): logo_art = """ +---------Welcome to-----------------------------+ -| __ _ v3.0 | +| __ _ v4.0 | | / /_____ __ _______(_)___ _____ ___ _____| | / //_/ _ \/ / / / ___/ / __ `/ __ \/ _ \/ ___/| | / ,< / __/ /_/ (__ ) / /_/ / / / / __/ / | @@ -48,7 +48,7 @@ def logo_ascii_art(): def meta_data(): print(color_text("To generate and manage keystore using keytool.\nAnd sign APK with custom keystore using apksigner.", 96)) - print(color_text("\nVersion:", 94), color_text("3.0", 92)) + print(color_text("\nVersion:", 94), color_text("4.0", 92)) print(color_text("Author:", 94), color_text("MuhammadRizwan", 92)) print(color_text("Repository:", 94), color_text("https://github.com/muhammadrizwan87/keysigner", 92)) print(color_text("Telegram Channel:", 94), color_text("https://TDOhex.t.me", 92)) @@ -99,27 +99,35 @@ def ensure_directory(user_path=None, dir_name='keystore', caller=None): print_red(f"An unexpected error occurred: {e}") raise Exception(f"An unexpected error occurred: {e}") -def validate_input(prompt, required=True, password=False, password_ck=False, path=False): +def validate_input(prompt, required=True, password=False, path=False, pass_opt=None, min_length=None): while True: - user_input = getpass(prompt) if (password or password_ck) else input(prompt) - - if user_input.lower() in ['q', 'x']: - print_blue("\nExiting keySigner. Goodbye!") - exit() - - if required and not user_input: - print_red("This field is required. Please enter a value.") - continue - - if (password and len(user_input) < 8) or (password_ck and len(user_input) < 6): - length = 8 if password else 6 - print_red(f"Password must be at least {length} characters long.") - continue - - if path: - user_input = os.path.abspath(user_input) - if not os.path.exists(user_input): - print_red("Invalid path. Please enter a valid path.") + if pass_opt: + user_input = getpass(cyan_text(prompt)) or pass_opt + if len(user_input) < min_length: + print_red(f"Password must be at least {min_length} characters long.") continue - + else: + user_input = getpass(prompt) if (password) else input(prompt) + + if user_input.lower() in ['q', 'x']: + print_blue("\nExiting keySigner. Goodbye!") + exit() + + if required and not user_input: + print_red("This field is required. Please enter a value.") + continue + + if password and len(user_input) < min_length: + print_red(f"Password must be at least {min_length} characters long.") + continue + + if path: + user_input = os.path.abspath(user_input) + if not os.path.exists(user_input): + print_red("Invalid path. Please enter a valid path.") + continue + if not os.access(user_input, os.R_OK): + print_red("Path is not accessible. Please check permissions.") + continue + return user_input \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 923046e..85e05b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "keysigner" -version = "3.0" +version = "4.0" description = "A command-line tool for keystore management and APK signing for Android developers." readme = "README.md" authors = [