Skip to content

Instantly share code, notes, and snippets.

@sspreitzer
Created August 13, 2022 12:47
Show Gist options
  • Save sspreitzer/2642c85f12396a5b6a623e95866d1a85 to your computer and use it in GitHub Desktop.
Save sspreitzer/2642c85f12396a5b6a623e95866d1a85 to your computer and use it in GitHub Desktop.
OnePassword entry deduplication in ruby
require 'open3'
require 'json'
require 'uri'
# MIT license, (c) Sascha Spreitzer, 2022
# !!! Create an export before using this tool !!!
# !!! You may loose all your passwords !!!
# !!! Use at your own risk !!!
# This script deduplicates OnePassword entries, which might have come
# as a result of imports, etc..
# To this day onepassword.com has not come up with a solution to that
# problem. This script is an attempt to workaround the problem.
# ruby op-dedup.rb
# Takes a minute to build a cache
# Deletes duplicates (by url hostname and username and password)
#
# Delete cache securely with: find .cache -type f -exec shred -u {} \;
def cmd(command)
cmd = command.split.first
opts = command.split[1..]
stdout, stderr, status = ::Open3.capture3(cmd, *opts)
raise "stdout: #{stdout}\nstderr: #{stderr}\nrc: #{status.exitstatus}" unless status.exitstatus == 0
return stdout
end
class OnePasswordDedup
attr_reader :items, :dups
def initialize()
@items = []
@dups = {}
begin
cmd("op whoami")
rescue
raise "You need to be logged in to onepassword cli: eval $(op signin)"
end
create_cache
end
def scanDups
@dups = {}
@items.each do |item|
next unless item.member?('id')
next if @dups.member?(item['id'])
next unless item.member?('urls')
next unless item.member?('fields')
next unless (item['fields'].select { |e| e['id'] == 'username'}).size >= 1
next unless (item['fields'].select { |e| e['id'] == 'password'}).size >= 1
id = item['id']
host = URI(item['urls'][0]['href']).host
username = (item['fields'].select { |e| e['id'] == 'username'})[0]['value']
password = (item['fields'].select { |e| e['id'] == 'password'})[0]['value']
@items.each do |item_|
next unless item_.member?('id')
next if item_['id'] == id
next unless item_.member?('urls')
next unless item_.member?('fields')
next unless (item_['fields'].select { |e| e['id'] == 'username'}).size >= 1
next unless (item_['fields'].select { |e| e['id'] == 'password'}).size >= 1
id_ = item_['id']
host_ = URI(item_['urls'][0]['href']).host
username_ = (item_['fields'].select { |e| e['id'] == 'username'})[0]['value']
password_ = (item_['fields'].select { |e| e['id'] == 'password'})[0]['value']
if host == host_ and username == username_ and password == password_
if @dups.member?(id)
@dups[id] << id_
else
@dups[id] = [id_]
end
end
end
end
return @dups
end
def deleteDups(archive=true)
if archive
command_archive = " --archive"
end
self.scanDups.each_pair do |id, dups|
dups.each do |dup|
puts "Deleting #{dup}"
cmd("op item delete #{dup}#{command_archive}")
end
end
end
private
def create_cache
Dir.mkdir('.cache') unless Dir.exists?('.cache')
item_list = JSON.parse(cmd("op item list --format=json"))
item_list.each do |item|
docache = true
id = item['id']
if File.exists?(File.join(".cache", "#{id}.json"))
if File.ctime(File.join(".cache", "#{id}.json")) >= Time.now-60*60
docache = false
end
end
if docache
item_ = JSON.parse(cmd("op item get #{id} --format=json"))
File.open(File.join(".cache", "#{id}.json"), 'w') do |file|
file.write(item_.to_json)
end
end
@items << JSON.parse(File.open(File.join(".cache", "#{id}.json")).read)
end
end
end
op = OnePasswordDedup.new
op.deleteDups
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment