From 487e62660b0f0e8740e4fa910931534f23223f92 Mon Sep 17 00:00:00 2001 From: Patrick Watson Date: Sun, 12 Jan 2025 22:33:09 -0500 Subject: [PATCH 1/5] lpass-att-export.sh: make sed work on MacOS The MacOS version of sed doesn't support the /s character class. It does support [:space:] though. Signed-off-by: Patrick Watson --- contrib/lpass-att-export.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/lpass-att-export.sh b/contrib/lpass-att-export.sh index df32c095..5e657452 100755 --- a/contrib/lpass-att-export.sh +++ b/contrib/lpass-att-export.sh @@ -47,7 +47,7 @@ if ! lpass status; then fi if [ -z ${id} ]; then - ids=$(lpass ls | sed -n "s/^.*id:\s*\([0-9]*\).*$/\1/p") + ids=$(lpass ls | sed -n "s/^.*id:[[:space:]]*\([0-9]*\).*$/\1/p") else ids=${id} fi From de71c0c88d4376a9433a9d2de62cc90b4026a54e Mon Sep 17 00:00:00 2001 From: Patrick Watson Date: Sun, 12 Jan 2025 22:42:14 -0500 Subject: [PATCH 2/5] lpass-att-export.sh: resolve shellcheck warnings Signed-off-by: Patrick Watson --- contrib/lpass-att-export.sh | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/contrib/lpass-att-export.sh b/contrib/lpass-att-export.sh index 5e657452..5ac4e4e9 100755 --- a/contrib/lpass-att-export.sh +++ b/contrib/lpass-att-export.sh @@ -33,34 +33,34 @@ fi command -v lpass >/dev/null 2>&1 || { echo >&2 "I require lpass but it's not installed. Aborting."; exit 1; } -if [ ! -d ${outdir} ]; then +if [ ! -d "${outdir}" ]; then echo "${outdir} does not exist. Exiting." exit 1 fi if ! lpass status; then - if [ -z ${email} ]; then + if [ -z "${email}" ]; then echo "No login data found, Please login with -l or use lpass login before." exit 1; fi - lpass login ${email} + lpass login "${email}" fi -if [ -z ${id} ]; then +if [ -z "${id}" ]; then ids=$(lpass ls | sed -n "s/^.*id:[[:space:]]*\([0-9]*\).*$/\1/p") else ids=${id} fi for id in ${ids}; do - show=$(lpass show ${id}) + show=$(lpass show "${id}") attcount=$(echo "${show}" | grep -c "att-") - path=$(lpass show --format="%/as%/ag%an" ${id} | uniq | tail -1) + path=$(lpass show --format="%/as%/ag%an" "${id}" | uniq | tail -1) - until [ ${attcount} -lt 1 ]; do - att=`lpass show ${id} | grep att- | sed "${attcount}q;d" | tr -d :` - attid=$(echo ${att} | awk '{print $1}') - attname=$(echo ${att} | awk '{print $2}') + until [ "${attcount}" -lt 1 ]; do + att=$(lpass show "${id}" | grep att- | sed "${attcount}q;d" | tr -d :) + attid=$(echo "${att}" | awk '{print $1}') + attname=$(echo "${att}" | awk '{print $2}') if [[ -z ${attname} ]]; then attname=${path#*/} @@ -74,11 +74,11 @@ for id in ${ids}; do out=${outdir}/${path}/${attcount}_${attname} fi - echo ${id} - ${path} ": " ${attid} "-" ${attname} " > " ${out} + echo "${id} - ${path} : ${attid} - ${attname} > ${out}" - lpass show --attach=${attid} ${id} --quiet > "${out}" + lpass show "--attach=${attid}" "${id}" --quiet > "${out}" - let attcount-=1 + (( attcount-=1 )) || true done done From ced016ed79eb465509f1bc134a9db4e79227af55 Mon Sep 17 00:00:00 2001 From: Patrick Watson Date: Sun, 12 Jan 2025 23:38:17 -0500 Subject: [PATCH 3/5] lpass-att-export.sh: optimize id gathering Use an export to find the ids of item that may contain attachments. This is better than showing every item since showing every item will prompt for your master password for every item with a reprompt set instead of only those we need to actually export. It will also be significantly faster on older hardware or when the master password PBKDF2 iterations are large. Signed-off-by: Patrick Watson --- contrib/lpass-att-export.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contrib/lpass-att-export.sh b/contrib/lpass-att-export.sh index 5ac4e4e9..c8830418 100755 --- a/contrib/lpass-att-export.sh +++ b/contrib/lpass-att-export.sh @@ -47,7 +47,9 @@ if ! lpass status; then fi if [ -z "${id}" ]; then - ids=$(lpass ls | sed -n "s/^.*id:[[:space:]]*\([0-9]*\).*$/\1/p") + # Get the ids of items that might have an attachment + # remove trailing carriage return if it's there + ids=$(lpass export --fields=id,attachpresent | grep ',1' | sed 's/,1\r\{0,1\}//') else ids=${id} fi From e0529859891a0d3fc7795f5d7ffcc5fe9a0cd8ce Mon Sep 17 00:00:00 2001 From: Patrick Watson Date: Sun, 26 Jan 2025 13:06:35 -0500 Subject: [PATCH 4/5] lpass-att-export.sh: fix truncation of attachment names lpass-att-export.sh used awk to read space separated tokens from the attachment description. This works fine for the id, but incorrectly truncats file names containing spaces. Using read not only solves this but also prevents needing 2 external program calls and a temp variable. Signed-off-by: Patrick Watson --- contrib/lpass-att-export.sh | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/contrib/lpass-att-export.sh b/contrib/lpass-att-export.sh index c8830418..35fdfa66 100755 --- a/contrib/lpass-att-export.sh +++ b/contrib/lpass-att-export.sh @@ -60,9 +60,8 @@ for id in ${ids}; do path=$(lpass show --format="%/as%/ag%an" "${id}" | uniq | tail -1) until [ "${attcount}" -lt 1 ]; do - att=$(lpass show "${id}" | grep att- | sed "${attcount}q;d" | tr -d :) - attid=$(echo "${att}" | awk '{print $1}') - attname=$(echo "${att}" | awk '{print $2}') + # switch to read because the original way truncated filenames containing spaces + read -r attid attname <<< "$(lpass show "${id}" | grep att- | sed "${attcount}q;d" | tr -d :)" if [[ -z ${attname} ]]; then attname=${path#*/} From 5f0e21fee150c6fc61e3f053b347b19edacff8cb Mon Sep 17 00:00:00 2001 From: Patrick Watson Date: Sun, 26 Jan 2025 14:36:37 -0500 Subject: [PATCH 5/5] Add format string option for file-system-safety Share and Group names are arbitrary strings and could contain characters that are not valid on file systems such as forward slashes ('/') or control characters. This applies to attachment file names also, though less commonly so since they usually will have a name from the uploading user's file system. Thus, when exporting attachments, these strings must be sanitized. This is painful to do in lpass-att-export.sh, so this commit introduces the new '_' format string modifier. This modifier works similarly to the existing '/' modifier, but causes instances of forward slashes, carriage returns, new lines, and tabs with an underscore in output. lpass-att-export.sh has been modified to use this new format string when a new-enough version of lpass is present. Signed-off-by: Patrick Watson --- contrib/lpass-att-export.sh | 10 +++++++- format.c | 49 ++++++++++++++++++++++++------------- lpass.1.txt | 6 ++++- 3 files changed, 46 insertions(+), 19 deletions(-) diff --git a/contrib/lpass-att-export.sh b/contrib/lpass-att-export.sh index 35fdfa66..47be1b64 100755 --- a/contrib/lpass-att-export.sh +++ b/contrib/lpass-att-export.sh @@ -33,6 +33,14 @@ fi command -v lpass >/dev/null 2>&1 || { echo >&2 "I require lpass but it's not installed. Aborting."; exit 1; } +# shellcheck disable=SC2034 # ignore unused version variables +IFS='.' read -r lpass_ver_major lpass_ver_minor lpass_ver_patch lpass_ver_vcs <<< "$(lpass --version | sed 's/LastPass CLI v//')" +if [ "${lpass_ver_major}" -gt 1 ] || [ "${lpass_ver_major}" -eq 1 ] && [ "${lpass_ver_minor}" -gt 6 ]; then + path_format="%/_as%/_ag%_an" +else + path_format="%/as%/ag%an" +fi + if [ ! -d "${outdir}" ]; then echo "${outdir} does not exist. Exiting." exit 1 @@ -57,7 +65,7 @@ fi for id in ${ids}; do show=$(lpass show "${id}") attcount=$(echo "${show}" | grep -c "att-") - path=$(lpass show --format="%/as%/ag%an" "${id}" | uniq | tail -1) + path=$(lpass show --format="${path_format}" "${id}" | uniq | tail -1) until [ "${attcount}" -lt 1 ]; do # switch to read because the original way truncated filenames containing spaces diff --git a/format.c b/format.c index 51269e8a..8731d312 100644 --- a/format.c +++ b/format.c @@ -80,19 +80,28 @@ char *format_timestamp(char *timestamp, bool utc) } static -void append_str(struct buffer *buf, char *str, bool add_slash) +void append_str(struct buffer *buf, char *str, bool add_slash, bool make_fs_safe) { + char *fs_parse_point = NULL; + /* can't set fs_parse_point here since the buffer might get reallocated by buffer_append_str */ + size_t orignal_len = buf->len; + if (!str || !strlen(str)) return; buffer_append_str(buf, str); + if (make_fs_safe) { + fs_parse_point = buf->bytes + orignal_len; + while ( (fs_parse_point = strpbrk(fs_parse_point, "/\r\n\t")) ) + *fs_parse_point = '_'; + } if (add_slash) buffer_append_char(buf, '/'); } static void format_account_item(struct buffer *buf, char fmt, - struct account *account, bool add_slash) + struct account *account, bool add_slash, bool make_fs_safe) { _cleanup_free_ char *name = NULL; _cleanup_free_ char *ts = NULL; @@ -100,47 +109,47 @@ void format_account_item(struct buffer *buf, char fmt, switch (fmt) { case 'i': /* id */ - append_str(buf, account->id, add_slash); + append_str(buf, account->id, add_slash, make_fs_safe); break; case 'n': /* shortname */ - append_str(buf, account->name, add_slash); + append_str(buf, account->name, add_slash, make_fs_safe); break; case 'N': /* fullname */ name = get_display_fullname(account); - append_str(buf, name, add_slash); + append_str(buf, name, add_slash, make_fs_safe); break; case 'u': /* username */ - append_str(buf, account->username, add_slash); + append_str(buf, account->username, add_slash, make_fs_safe); break; case 'p': /* password */ - append_str(buf, account->password, add_slash); + append_str(buf, account->password, add_slash, make_fs_safe); break; case 'm': /* mtime */ ts = format_timestamp(account->last_modified_gmt, true); - append_str(buf, ts, add_slash); + append_str(buf, ts, add_slash, make_fs_safe); break; case 'U': /* last touch time */ ts = format_timestamp(account->last_touch, false); - append_str(buf, ts, add_slash); + append_str(buf, ts, add_slash, make_fs_safe); break; case 's': /* sharename */ if (account->share) - append_str(buf, account->share->name, add_slash); + append_str(buf, account->share->name, add_slash, make_fs_safe); break; case 'g': /* group name */ - append_str(buf, account->group, add_slash); + append_str(buf, account->group, add_slash, make_fs_safe); break; case 'l': /* URL */ - append_str(buf, account->url, add_slash); + append_str(buf, account->url, add_slash, make_fs_safe); break; default: break; @@ -149,12 +158,12 @@ void format_account_item(struct buffer *buf, char fmt, void format_field_item(struct buffer *buf, char fmt, char *field_name, char *field_value, - bool add_slash) + bool add_slash, bool make_fs_safe) { if (fmt == 'n' && field_name) { - append_str(buf, field_name, add_slash); + append_str(buf, field_name, add_slash, make_fs_safe); } else if (fmt == 'v' && field_value) { - append_str(buf, field_value, add_slash); + append_str(buf, field_value, add_slash, make_fs_safe); } } @@ -165,6 +174,7 @@ void format_field(struct buffer *buf, const char *format_str, const char *p = format_str; bool in_format = false; bool add_slash = false; + bool make_fs_safe = false; while (*p) { char ch = *p++; @@ -188,6 +198,10 @@ void format_field(struct buffer *buf, const char *format_str, /* append trailing slash, if nonempty */ add_slash = true; continue; + case '_': + /* transform slashes to underscores in field value */ + make_fs_safe = true; + continue; case 'f': /* field name/value */ if (!*p) { @@ -196,7 +210,7 @@ void format_field(struct buffer *buf, const char *format_str, break; } ch = *p++; - format_field_item(buf, ch, field_name, field_value, add_slash); + format_field_item(buf, ch, field_name, field_value, add_slash, make_fs_safe); break; case 'a': /* account item */ @@ -206,13 +220,14 @@ void format_field(struct buffer *buf, const char *format_str, break; } ch = *p++; - format_account_item(buf, ch, account, add_slash); + format_account_item(buf, ch, account, add_slash, make_fs_safe); break; default: buffer_append_char(buf, '%'); buffer_append_char(buf, ch); } add_slash = false; + make_fs_safe = false; in_format = false; } } diff --git a/lpass.1.txt b/lpass.1.txt index 870d7986..1051af05 100644 --- a/lpass.1.txt +++ b/lpass.1.txt @@ -171,7 +171,11 @@ the following placeholders: A slash can be added between the '%' and the placeholder to indicate that a slash should be appended, only if the printed value is expanded to a non-empty string. For example, this command will properly show the full path to -an account: `lpass ls --format="%/as%/ag%an"`. +an account: `lpass ls --format="%/as%/ag%an"`. In the same way, an underscore may +be added between the '%' and the placeholder to indicate that the value should +be made file-system-safe by replacing forward slashes, tabs, carriage returns, +and new lines with an underscore. This may be combined with the slash. For +example: `lpass show --format="%/_as%/_ag%_an"`. Modifying ~~~~~~~~~