Skip to content

Instantly share code, notes, and snippets.

@10bn

10bn/convert.py Secret

Created September 25, 2024 09:33
Show Gist options
  • Save 10bn/a18248bb8e96eeda98f1071ad3c38059 to your computer and use it in GitHub Desktop.
Save 10bn/a18248bb8e96eeda98f1071ad3c38059 to your computer and use it in GitHub Desktop.
Convert bitwarden to icloud
import csv
import json
import re
from urllib.parse import urlparse, urlunparse, urlencode, parse_qs
# ==============================
# Placeholder Definitions
# ==============================
# Placeholder prefix for usernames when missing
PLACEHOLDER_USERNAME_PREFIX = 'placeholder_user_'
# Placeholder value for passwords when missing
PLACEHOLDER_PASSWORD = 'placeholder_password_'
# Placeholder prefix for URLs when missing or invalid
PLACEHOLDER_URL_PREFIX = 'https://placeholder.example.com/'
# ==============================
# Function Definitions
# ==============================
def format_totp_url(totp, hostname, username):
"""
Formats a TOTP (Time-based One-Time Password) URL for use in iCloud Keychain.
Parameters:
totp (str): The TOTP secret or URL.
hostname (str): The hostname of the website.
username (str): The username for the account.
Returns:
str: A formatted otpauth URL.
"""
if totp.startswith('otpauth://'):
# Extract the secret from the existing otpauth URL
parsed_totp = urlparse(totp)
totp = parse_qs(parsed_totp.query)['secret'][0]
# Normalize the TOTP secret
totp = totp.upper().replace(' ', '')
# Build the otpauth URL
return urlunparse((
'otpauth',
'totp',
f'{hostname}:{username}',
'',
urlencode({
'secret': totp,
'issuer': hostname,
'algorithm': 'SHA1',
'digits': '6',
'period': '30',
}),
''
))
def sanitize_string(s):
"""
Sanitizes a string to be URL and filename safe.
Parameters:
s (str): The string to sanitize.
Returns:
str: The sanitized string.
"""
# Remove any characters that are not letters, numbers, underscores, or hyphens
value = re.sub(r'[^a-zA-Z0-9_\-]', '_', s)
print(f'Sanitized "{s}" to "{value}"')
return value
def format_notes(bitwarden_record):
"""
Formats the notes and custom fields from a BitWarden record.
Parameters:
bitwarden_record (dict): The BitWarden record.
Returns:
str: A string containing the notes and custom fields.
"""
notes_lines = []
name = bitwarden_record.get('name', 'Unknown')
# Get custom fields if they exist
fields = bitwarden_record.get('fields', [])
if isinstance(fields, list):
for field_dict in fields:
if isinstance(field_dict, dict):
custom_field_name = field_dict.get('name', '')
custom_field_value = field_dict.get('value', '')
# Ensure both are strings before stripping
if isinstance(custom_field_name, str):
custom_field_name = custom_field_name.strip()
else:
custom_field_name = ''
if isinstance(custom_field_value, str):
custom_field_value = custom_field_value.strip()
else:
custom_field_value = ''
if custom_field_name and custom_field_value:
notes_lines.append(f"{custom_field_name}: {custom_field_value}")
# Get the standard notes field
notes = bitwarden_record.get('notes', '')
if isinstance(notes, str) and notes.strip():
notes_lines.append(notes.strip())
# Combine all notes and custom fields
return "\n\n".join(notes_lines)
def convert_bitwarden_record_to_icloud_records(bitwarden_record):
"""
Converts a single BitWarden record into one or more iCloud Keychain records.
Parameters:
bitwarden_record (dict): The BitWarden record.
Returns:
list: A list of iCloud Keychain records (each is a list of fields).
"""
name = bitwarden_record.get('name', 'Unknown')
sanitized_name = sanitize_string(name) or 'Unknown'
bitwarden_record_type = bitwarden_record.get('type', 1)
# Proceed only if the type is one of the known types
if bitwarden_record_type not in [1, 2, 3, 4]:
print(f'Skipping "{name}" because it is of an unknown type (it has type `{bitwarden_record_type}`)')
return []
# Get the login details
login = bitwarden_record.get('login', {})
if not isinstance(login, dict):
login = {}
# Get the username, ensure it's a string
username = login.get('username', '')
if not isinstance(username, str):
username = ''
username = username.strip()
if not username:
print(f'Entry "{name}" is missing a username. Using placeholder.')
username = f'{PLACEHOLDER_USERNAME_PREFIX}{sanitized_name}'
# Get the password, ensure it's a string
password = login.get('password', '')
if not isinstance(password, str):
password = ''
password = password.strip()
if not password:
print(f'Entry "{name}" is missing a password. Using placeholder.')
password = PLACEHOLDER_PASSWORD
# Get the list of URIs, or use a placeholder if missing
uri_dicts = login.get('uris', [])
if not isinstance(uri_dicts, list) or not uri_dicts:
print(f'Entry "{name}" does not have website URLs. Using placeholder URL.')
# Create a unique placeholder URL using the sanitized name
uri_dicts = [{'uri': f'{PLACEHOLDER_URL_PREFIX}{sanitized_name}'}]
# Format notes and custom fields
notes = format_notes(bitwarden_record)
icloud_accounts = []
for uri_dict in uri_dicts:
# Get the URI, ensure it's a string
uri = uri_dict.get('uri', '')
if not isinstance(uri, str):
uri = ''
uri = uri.strip()
if not uri:
print(f'Could not process URL in "{name}" because it is invalid. Using placeholder URL.')
uri = f'{PLACEHOLDER_URL_PREFIX}{sanitized_name}'
if not uri.startswith('http://') and not uri.startswith('https://'):
print(f'URI "{uri}" in "{name}" does not start with http:// or https://. Prepending https://')
uri = 'https://' + uri
try:
# Parse the URI to extract components
parsed_uri = urlparse(uri)
except Exception as e:
print(f'Could not process URL "{uri}" in "{name}" because it is not valid: {e}')
continue
# Format TOTP if available and valid
totp = ''
if 'totp' in login:
totp_value = login['totp']
if isinstance(totp_value, str) and totp_value.strip():
totp = format_totp_url(totp_value, parsed_uri.hostname or 'example.com', username)
# Append the record to the list
icloud_accounts.append([
name,
uri,
username,
password,
notes,
totp
])
return icloud_accounts
# ==============================
# Main Script Execution
# ==============================
# Open and parse the BitWarden JSON export file
with open('bitwarden.json', 'r', encoding='utf-8') as bitwarden_export_json_file:
parsed_bitwarden_export = json.load(bitwarden_export_json_file)
print(f'Found {len(parsed_bitwarden_export["items"])} accounts in BitWarden')
icloud_records = []
# Convert each BitWarden record to iCloud format
for bitwarden_record in parsed_bitwarden_export['items']:
icloud_records.extend(convert_bitwarden_record_to_icloud_records(bitwarden_record))
print(f'Saving {len(icloud_records)} accounts to iCloud CSV')
# Write the iCloud records to a CSV file
with open('icloud.csv', 'w', encoding='UTF8', newline='') as csv_file:
writer = csv.writer(csv_file)
# Write the header row
writer.writerow(['Title', 'URL', 'Username', 'Password', 'Notes', 'OTPAuth'])
# Write the data rows
writer.writerows(icloud_records)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment