Skip to content
This repository was archived by the owner on Mar 20, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ sudo: false
language: ruby
rvm:
- 2.5.0
before_install: gem install bundler -v 1.16.0
before_install: gem install bundler -v 1.16.4
36 changes: 18 additions & 18 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4,45 +4,45 @@ PATH
bitwapi (0.1.0)
jwt (~> 1.5, >= 1.5.4)
pbkdf2-ruby
rest-client (~> 2.1.0.rc1)
rest-client (~> 2.1.0)

GEM
remote: https://rubygems.org/
specs:
diff-lcs (1.3)
domain_name (0.5.20170404)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
http-accept (1.7.0)
http-cookie (1.0.3)
domain_name (~> 0.5)
jwt (1.5.6)
mime-types (3.1)
mime-types (3.2.2)
mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521)
mime-types-data (3.2019.0331)
netrc (0.11.0)
pbkdf2-ruby (0.2.1)
rake (10.5.0)
rest-client (2.1.0.rc1)
rest-client (2.1.0)
http-accept (>= 1.7.0, < 2.0)
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
rspec (3.7.0)
rspec-core (~> 3.7.0)
rspec-expectations (~> 3.7.0)
rspec-mocks (~> 3.7.0)
rspec-core (3.7.1)
rspec-support (~> 3.7.0)
rspec-expectations (3.7.0)
rspec (3.8.0)
rspec-core (~> 3.8.0)
rspec-expectations (~> 3.8.0)
rspec-mocks (~> 3.8.0)
rspec-core (3.8.2)
rspec-support (~> 3.8.0)
rspec-expectations (3.8.4)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.7.0)
rspec-mocks (3.7.0)
rspec-support (~> 3.8.0)
rspec-mocks (3.8.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.7.0)
rspec-support (3.7.0)
rspec-support (~> 3.8.0)
rspec-support (3.8.2)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.4)
unf_ext (0.0.7.6)

PLATFORMS
ruby
Expand All @@ -54,4 +54,4 @@ DEPENDENCIES
rspec (~> 3.0)

BUNDLED WITH
1.16.0
1.16.4
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,26 +33,26 @@ api = Bitwapi::API.official
Or with your own unofficial Bitwarden-ruby instance:
```ruby
require 'bitwapi'
api = Bitwapi.API.unofficial("https://mybitwarden.example.com")
api = Bitwapi::API.unofficial("https://mybitwarden.example.com")
```

### Register a new account

```
```ruby
# hint and name are optional
api.register(email, password, hint:'hint for password', name:'user name')

```

### Login a new device

```
```ruby
# device_name is optional (default: bitwapi/version)
api.login(email, password, device_name: "my device")
```

You probably shouldn't login a new device each time you want to access your vault (please don't, at least if you are using the official Bitwarden servers. I don't want them to ban this unofficial client because of abuse from your part). Once you have credentials, save them and use them for future access:
```

```ruby
require 'json'
require 'bitwapi'

Expand All @@ -66,22 +66,22 @@ File.write("mycredentials.json", credentials.to_json)
json_credentials = File.read("mycredentials.json")
credentials = JSON.parse(json_credentials, symbolize_names: true)
api = Bitwapi::API.new(credentials)

```

Bitwapi automaticaly refresh the access token when needed. You do not have to care about all that.


### Get the vault from server

```
```ruby
api = Bitwapi::API.new(credentials)
vault = api.get_vault
```


### Get ciphers from the vault
```

```ruby
# all ciphers
ciphers = vault.ciphers.to_a
id = ciphers[0].id
Expand Down
2 changes: 1 addition & 1 deletion bitwapi.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Gem::Specification.new do |spec|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]

spec.add_dependency "rest-client", "~> 2.1.0.rc1"
spec.add_dependency "rest-client", "~> 2.1.0"
spec.add_dependency "pbkdf2-ruby"
spec.add_dependency "jwt", '~> 1.5', '>= 1.5.4'

Expand Down
36 changes: 26 additions & 10 deletions lib/bitwapi/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def self.official(options={})
def self.unofficial(base, options={})
urls = {
base_url: "#{base}/api",
identity_url: "#{base}/identity_api",
identity_url: "#{base}/identity",
icons_url: "#{base}/icons",
}
self.new(urls.merge(default_options).merge(options))
Expand Down Expand Up @@ -65,12 +65,14 @@ def credentials
}
end

def register(email, password, name:nil, hint:nil, access_token:nil)
def register(email, password, name:nil, hint:nil, access_token:nil, kdf:nil, iterations:nil)
kdf ||= Bitwapi::Crypto::PBKDF2_SHA256
iterations ||= Bitwapi::Crypto::DEFAULT_ITERATIONS[kdf]
destination = "#{@base_url}/accounts/register"
internal_key = @crypto.make_master_key(password, email)
internal_key = @crypto.make_master_key(password, email, kdf, iterations)
key = @crypto.make_enc_key(internal_key)
master_password_hash = @crypto.hash_password(password, email)
transport.json_post(destination, {
master_password_hash = @crypto.hash_password(password, email, kdf, iterations)
@transport.json_post(destination, {
name: name,
email: email,
masterPasswordHash: master_password_hash,
Expand All @@ -84,9 +86,21 @@ def generate_identifier
SecureRandom.uuid
end

def login(email, password, device_type: @device_type, device_identifier: generate_identifier, device_name: @agent_string, device_push_token: "", client_id:"browser", two_factor_provider: nil, two_factor_token: nil, two_factor_remember: 1)
def prelogin(email)
destination = "#{@base_url}/accounts/prelogin"
@transport.json_post(destination, {
email: email
}, { 'Authorization' => "none"})
end

def login(email, password, device_type: @device_type, device_identifier: generate_identifier, device_name: @agent_string, device_push_token: "", client_id:"browser", two_factor_provider: nil, two_factor_token: nil, two_factor_remember: 1, kdf_type:nil, kdf_iterations:nil)
if kdf_type.nil? || kdf_iterations.nil?
data = prelogin(email)
kdf_type = data[:Kdf] || Bitwapi::Crypto::PBKDF2_SHA256
kdf_iterations = data[:KdfIterations] || Bitwapi::Crypto::DEFAULT_ITERATIONS[kdf_type]
end
destination = "#{@identity_url}/connect/token"
master_password_hash = @crypto.hash_password(password, email)
master_password_hash = @crypto.hash_password(password, email, kdf_type, kdf_iterations)
grant = {
grant_type: 'password',
username: email,
Expand All @@ -105,12 +119,13 @@ def login(email, password, device_type: @device_type, device_identifier: generat
twoFactorRemember: two_factor_remember,
})
end
transport.post(destination, grant, { 'Authorization' => "none"}).tap do |resp|
@transport.post(destination, grant, { 'Authorization' => "none"}).tap do |resp|
resp[:expire_at] = get_token_expiration(resp[:access_token])
@access_token = resp[:access_token]
@refresh_token = resp[:refresh_token]
@expire_at = resp[:expire_at]
end
credentials
end

def get_valid_token
Expand Down Expand Up @@ -143,7 +158,7 @@ def get_vault

def refresh_token
destination = "#{@identity_url}/connect/token"
transport.post(destination, {
@transport.post(destination, {
"grant_type": "refresh_token",
"client_id": "browser",
"refresh_token": @refresh_token,
Expand All @@ -153,8 +168,9 @@ def refresh_token
@refresh_token = resp[:refresh_token]
@expire_at = resp[:expire_at]
end
credentials
end

end

end
end
22 changes: 11 additions & 11 deletions lib/bitwapi/cipher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ module Bitwapi
class Cipher

TYPE = nil
ATTRIBUTES = [ ]
ATTRIBUTES = []

def empty_block
{
def empty_block
{
CollectionIds: [],
FolderId: nil,
Favorite: true,
Favorite: false,
Edit: true,
Type: self.class::TYPE,
Id: nil,
Expand All @@ -21,22 +21,22 @@ def empty_block
Attachments: nil,
OrganizationUseTotp: false,
RevisionDate: nil,
Object: "cipherDetails",
Object: "cipher"
}
end

def empty_data_block
{
Name: nil,
Notes: nil,
Fields: [ ],
Fields: []
}.merge( self.class::ATTRIBUTES.map {|attribute| [attribute, nil] }.to_h )
end

def self.attributes(*names)
self.const_set(:ATTRIBUTES, names)
names.each do |title|
underscore = title.to_s.gsub(/([A-Z])([A-Z]*[a-z]*)/){"_#{$1.downcase}#{$2}"}[1..-1]
names.each do |title|
underscore = title.to_s.gsub(/([A-Z])([A-Z]*[a-z]*)/) { "_#{$1.downcase}#{$2}" }[1..-1]
define_method(underscore.to_sym) { @data[:Data][title.to_sym] }
end
end
Expand All @@ -54,7 +54,7 @@ def self.from_encrypted(data, &block)
klass.new(data)
end

def initialize(data, &block)
def initialize(data)
@data = data
end

Expand Down Expand Up @@ -96,7 +96,7 @@ def type
end

def revision_date
@data[:RevisionDate] ? Time.parse(@data[:RevisionDate]+"Z") : nil
@data[:RevisionDate] ? Time.parse(@data[:RevisionDate]) : nil
end

def attachments
Expand All @@ -117,4 +117,4 @@ def fields

end

end
end
42 changes: 30 additions & 12 deletions lib/bitwapi/crypto.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,36 @@ class Crypto
RSA2048_OAEPSHA256_HMACSHA256_B64 = 5
RSA2048_OAEPSHA1_HMACSHA256_B64 = 6

def make_master_key(password, email)
make_key(password, email.downcase)
PBKDF2_SHA256 = 0
DEFAULT_ITERATIONS = {
PBKDF2_SHA256 => 100_000
}.freeze
ITERATION_RANGES = {
PBKDF2_SHA256 => 5_000..1_000_000
}.freeze

def make_master_key(password, email, kdf_type, kdf_iterations)
make_key(password, email.downcase, kdf_type, kdf_iterations)
end

def make_key(password, salt)
PBKDF2.new(:password => password, :salt => salt,
:iterations => 5000, :hash_function => OpenSSL::Digest::SHA256,
:key_length => (256/8)).bin_string
def make_key(password, salt, kdf_type, kdf_iterations)
case kdf_type
when PBKDF2_SHA256
range = ITERATION_RANGES[kdf_type]
unless range.include?(kdf_iterations)
raise "PBKDF2 iterations must be between #{range}"
end

PBKDF2.new(:password => password, :salt => salt,
:iterations => kdf_iterations, :hash_function => OpenSSL::Digest::SHA256,
:key_length => (256/8)).bin_string
else
raise "unknown kdf type #{kdf_type.inspect}"
end
end

def hash_password(password, salt)
key = make_key(password, salt)
def hash_password(password, salt, kdf_type, kdf_iterations)
key = make_master_key(password, salt, kdf_type, kdf_iterations)
Base64.strict_encode64(PBKDF2.new(:password => key, :salt => password,
:iterations => 1, :key_length => 256/8,
:hash_function => OpenSSL::Digest::SHA256).bin_string)
Expand Down Expand Up @@ -80,7 +98,7 @@ def decrypt(str, key, mac_key=nil)
key, mac_key = split_key(key) if mac_key.nil?

type, iv, ct, mac = decompose_cipher_string(str)

case type
when AESCBC256_B64, AESCBC256_HMACSHA256_B64

Expand All @@ -104,8 +122,8 @@ def decrypt(str, key, mac_key=nil)
end
end

def decrypted_key(enc_key, email, password)
master_key = make_master_key(password, email)
def decrypted_key(enc_key, email, password, kdf_type, kdf_iterations)
master_key = make_master_key(password, email, kdf_type, kdf_iterations)
decrypt(enc_key, master_key, nil)
end

Expand Down Expand Up @@ -133,4 +151,4 @@ def decompose_cipher_string(str)

end

end
end
8 changes: 5 additions & 3 deletions lib/bitwapi/vault.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ def initialize(data, password:nil)
unlock!(password) if password
end

def unlock!(password)
def unlock!(password, kdf:nil, iterations:nil)
kdf ||= Bitwapi::Crypto::PBKDF2_SHA256
iterations ||= Bitwapi::Crypto::DEFAULT_ITERATIONS[kdf]
email = @data[:Profile][:Email]
@key = @crypto.decrypted_key(@data[:Profile][:Key], email, password)
@key = @crypto.decrypted_key(@data[:Profile][:Key], email, password, kdf, iterations)
true
end

Expand Down Expand Up @@ -49,4 +51,4 @@ def decrypt_data(data)

end

end
end
Loading