An unofficial, mostly Bitwarden-compatible API server written in Ruby (Sinatra and ActiveRecord)
1#!/usr/bin/env ruby
2#
3# Copyright (c) 2018 joshua stein <jcs@jcs.org>
4#
5# Permission to use, copy, modify, and distribute this software for any
6# purpose with or without fee is hereby granted, provided that the above
7# copyright notice and this permission notice appear in all copies.
8#
9# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16#
17
18require File.realpath(File.dirname(__FILE__) + "/../lib/rubywarden.rb")
19require "getoptlong"
20
21def usage
22 puts "usage: #{$0} -u user@example.com"
23 exit 1
24end
25
26username = nil
27
28begin
29 GetoptLong.new(
30 [ "--user", "-u", GetoptLong::REQUIRED_ARGUMENT ],
31 ).each do |opt,arg|
32 case opt
33 when "--user"
34 username = arg
35 end
36 end
37
38rescue GetoptLong::InvalidOption
39 usage
40end
41
42if !username
43 usage
44end
45
46@u = User.find_by_email(username)
47if !@u
48 raise "can't find existing User record for #{username.inspect}"
49end
50
51print "master password for #{@u.email}: "
52system("stty -echo")
53password = STDIN.gets.chomp
54system("stty echo")
55print "\n"
56
57unless @u.has_password_hash?(Bitwarden.hashPassword(password, @u.email,
58Bitwarden::KDF::TYPES[@u.kdf_type], @u.kdf_iterations))
59 raise "master password does not match stored hash"
60end
61
62new_master = nil
63new_master_conf = nil
64new_master_hint = nil
65
66while new_master.to_s == "" || (new_master != new_master_conf)
67 print "new master password: "
68 system("stty -echo")
69 new_master = STDIN.gets.chomp
70 system("stty echo")
71 print "\n"
72
73 print "new master password (again): "
74 system("stty -echo")
75 new_master_conf = STDIN.gets.chomp
76 system("stty echo")
77 print "\n"
78
79 if new_master == new_master_conf
80 print "new master password hint (optional): "
81 system("stty -echo")
82 new_master_hint = STDIN.gets.chomp
83 system("stty echo")
84 print "\n"
85 else
86 puts "error: passwords do not match"
87 end
88end
89
90new_kdf_iterations = nil
91while new_kdf_iterations.to_s == ""
92 print "kdf iterations (currently #{@u.kdf_iterations}, default " <<
93 "#{Bitwarden::KDF::DEFAULT_ITERATIONS[@u.kdf_type]}): "
94 new_kdf_iterations = STDIN.gets.chomp
95
96 if new_kdf_iterations.to_s == ""
97 new_kdf_iterations = Bitwarden::KDF::DEFAULT_ITERATIONS[@u.kdf_type]
98 break
99 elsif !(r = Bitwarden::KDF::ITERATION_RANGES[@u.kdf_type]).
100 include?(new_kdf_iterations.to_i)
101 puts "iterations must be between #{r}"
102 new_kdf_iterations = nil
103 else
104 new_kdf_iterations = new_kdf_iterations.to_i
105 break
106 end
107end
108
109@u.update_master_password(password, new_master, new_kdf_iterations)
110@u.password_hint = new_master_hint
111if !@u.save
112 puts "error saving new password"
113end
114
115puts "master password changed"