Janik von Rotz


3 min read

Migrate From Pass to KeepassXC

In 2017 I migrated from KeePass to Pass and now migrated back to KeePassXC. For almost 9 years I was a happy pass user. It still does a very good job and keeping password management simple. But setting up pass on a new device was always a bit difficult.

Whenever I wanted to access my passwords on a new deceive I had to:

Moreover, I missed two essential features:

And KeePassXC supports both of these cases. In addition I already used KeePassXC:

Export

There were over 400 entries in my pass store. A manual migration was out of question. I created script that parsed the pass entries and handled the following cases:

And here is the script / bash function:

export-keepass-csv() {
    if test -z "$1"; then
        echo "\$1 is empty." >&2
        return 1
    fi

    OUTPUT_FILE="$1"
    PASS_DIR="${PASSWORD_STORE_DIR:-$HOME/.password-store}"

    # Validate PASS_DIR exists
    if [[ ! -d "$PASS_DIR" ]]; then
        echo "Error: Password store directory not found: $PASS_DIR" >&2
        return 1
    fi

    # Header (already properly quoted)
    echo '"Group","Title","Username","Password","URL","Notes","TOTP"' > "$OUTPUT_FILE"

    # Use process substitution to avoid subshell
    while read -r FILE; do
        echo "Parse pass file: ${FILE#$PASS_DIR/}"

        REL_PATH="${FILE#$PASS_DIR/}"
        REL_PATH="${REL_PATH%.gpg}"
        GROUP="Root/Pass/$(dirname $REL_PATH)"
        FOLDER="$(dirname "$GROUP")"
        TITLE="$(basename "$REL_PATH")"
        ENTRY="$(pass show "$REL_PATH")"

        # Extract password (first line)
        PASSWORD="$(printf '%s' "$ENTRY" | head -n1)"

        # Initialize fields
        USERNAME=""
        URL="https://$TITLE"
        NOTES=""
        TOTP=""

        # Parse additional key-value lines (skip first line)
        while IFS= read -r LINE; do
            case "$LINE" in
                username:*) USERNAME="${LINE#username: }" ;;
                Username:*) USERNAME="${LINE#Username: }" ;;
                notes:*)     NOTES="${LINE#notes: }" ;;
                Notes:*)     NOTES="${LINE#Notes: }" ;;
                url:*)       URL="${LINE#url: }" ;;
                Url:*)       URL="${LINE#Url: }" ;;
                totp:*)      TOTP="${LINE#totp: }" ;;
            esac
        done <<< "$(printf '%s\n' "$ENTRY" | tail -n +2)"

        # If password starts with otpauth://, extract the secret and set as TOTP
        if [[ "$PASSWORD" == otpauth://* ]]; then
            # Extract secret from otpauth URL using query parameter parsing
            SECRET=$(echo "$PASSWORD" | sed -n 's/.*secret=\([^&]*\).*/\1/p' | tr '[:lower:]' '[:upper:]')
            if [[ -n "$SECRET" ]]; then
                TOTP="$SECRET"
            fi
        fi

        # Use fallback username based on folder
        if [[ "$USERNAME" == "" ]]; then
            if [[ "$FOLDER" == *"private"* ]]; then
                USERNAME="$FALLBACK_USERNAME_private"
            elif [[ "$FOLDER" == *"mint-system"* ]]; then
                USERNAME="$FALLBACK_USERNAME_mint_system"
            elif [[ "$FOLDER" == *"sozialinfo"* ]]; then
                USERNAME="$FALLBACK_USERNAME_sozialinfo"
            fi
        fi

        # Function to escape double quotes and wrap in quotes for CSV
        PASSWORD_CSV=$(printf '%s' "$PASSWORD" | sed 's/"/""/g; s/.*/"&"/')
        USERNAME_CSV=$(printf '%s' "$USERNAME" | sed 's/"/""/g; s/.*/"&"/')
        NOTES_CSV=$(printf '%s' "$NOTES" | sed 's/"/""/g; s/.*/"&"/')
        TOTP_CSV=$(printf '%s' "$TOTP" | sed 's/"/""/g; s/.*/"&"/')

        # URL is usually safe, but still escape if needed
        URL_CSV=$(printf '%s' "$URL" | sed 's/"/""/g; s/.*/"&"/')

        # Group and Title should also be escaped
        GROUP_CSV=$(printf '%s' "$GROUP" | sed 's/"/""/g; s/.*/"&"/')
        TITLE_CSV=$(printf '%s' "$TITLE" | sed 's/"/""/g; s/.*/"&"/')

        # Write to CSV
        printf '%s,%s,%s,%s,%s,%s,%s\n' \
            "$GROUP_CSV" \
            "$TITLE_CSV" \
            "$USERNAME_CSV" \
            "$PASSWORD_CSV" \
            "$URL_CSV" \
            "$NOTES_CSV" \
            "$TOTP_CSV" >> "$OUTPUT_FILE"

    done < <(find "$PASS_DIR" -name "*.gpg" ! -name ".gpg" -print)

    echo "All pass entries exported to $OUTPUT_FILE."
}

Note that this bash function was added to a task script and called with ./task export-keepass-csv in context of the .password-store folder.

I am quite happy with the decision to go back using KeePass. The browser plugin and the passkey support are a great help.

Category: security
Tags: 100daystooffload , passkeys , password , keepass
Edit Page / Show Statistic