keybase-core 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +19 -0
  3. data/.ruby-version +1 -0
  4. data/.travis.yml +3 -0
  5. data/Gemfile +2 -0
  6. data/Gemfile.lock +49 -0
  7. data/LICENSE +23 -0
  8. data/README.md +137 -0
  9. data/Rakefile +20 -0
  10. data/keybase.gemspec +25 -0
  11. data/lib/keybase.rb +49 -0
  12. data/lib/keybase/crypto.rb +20 -0
  13. data/lib/keybase/error.rb +37 -0
  14. data/lib/keybase/models/dump.rb +33 -0
  15. data/lib/keybase/models/user.rb +168 -0
  16. data/lib/keybase/request/base.rb +32 -0
  17. data/lib/keybase/request/dump/all.rb +10 -0
  18. data/lib/keybase/request/dump/latest.rb +10 -0
  19. data/lib/keybase/request/key/add.rb +13 -0
  20. data/lib/keybase/request/key/revoke.rb +14 -0
  21. data/lib/keybase/request/root/get_salt_and_login_session.rb +12 -0
  22. data/lib/keybase/request/root/login.rb +14 -0
  23. data/lib/keybase/request/sig/post_auth.rb +13 -0
  24. data/lib/keybase/request/user/lookup.rb +10 -0
  25. data/lib/keybase/response.rb +14 -0
  26. data/lib/keybase/token_store.rb +32 -0
  27. data/test/all.rb +6 -0
  28. data/test/fixtures/example_dump.json +9 -0
  29. data/test/fixtures/example_user.json +50 -0
  30. data/test/integration/dumps_test.rb +23 -0
  31. data/test/integration/keys_test.rb +26 -0
  32. data/test/integration/login_users_test.rb +18 -0
  33. data/test/integration/lookup_users_test.rb +42 -0
  34. data/test/integration_test_helper.rb +6 -0
  35. data/test/test_helper.rb +17 -0
  36. data/test/unit/lib/keybase/crypto_test.rb +45 -0
  37. data/test/unit/lib/keybase/error_test.rb +32 -0
  38. data/test/unit/lib/keybase/models/dump_test.rb +31 -0
  39. data/test/unit/lib/keybase/models/user_test.rb +136 -0
  40. data/test/unit/lib/keybase/requests/base_test.rb +60 -0
  41. data/test/unit/lib/keybase/requests/dump/all_test.rb +17 -0
  42. data/test/unit/lib/keybase/requests/dump/latest_test.rb +17 -0
  43. data/test/unit/lib/keybase/requests/key/add_test.rb +19 -0
  44. data/test/unit/lib/keybase/requests/key/revoke_test.rb +19 -0
  45. data/test/unit/lib/keybase/requests/root/get_salt_test.rb +23 -0
  46. data/test/unit/lib/keybase/requests/root/login_test.rb +19 -0
  47. data/test/unit/lib/keybase/requests/sig/post_auth_test.rb +19 -0
  48. data/test/unit/lib/keybase/requests/user/lookup_test.rb +17 -0
  49. data/test/unit/lib/keybase/response_test.rb +23 -0
  50. data/test/unit/lib/keybase/token_store_test.rb +25 -0
  51. data/test/unit/lib/keybase_test.rb +25 -0
  52. data/test/vcr_cassettes/dumps.yml +49418 -0
  53. data/test/vcr_cassettes/keys.yml +349 -0
  54. data/test/vcr_cassettes/user_login.yml +176 -0
  55. data/test/vcr_cassettes/user_lookup_foo.yml +89 -0
  56. data/test/vcr_cassettes/user_lookup_invalid.yml +52 -0
  57. data/test/vcr_cassettes/user_lookup_missing.yml +52 -0
  58. data/test/vcr_cassettes/user_lookup_not_found.yml +55 -0
  59. metadata +200 -0
@@ -0,0 +1,168 @@
1
+ module Keybase
2
+ # A Keybase user containing all attributes you have permission to see
3
+ class User
4
+
5
+ attr_reader :id, :basics, :invitation_stats, :profile, :emails,
6
+ :public_keys, :private_keys
7
+
8
+ def initialize(params)
9
+ @id = params['id']
10
+ @invitation_stats = OpenStruct.new(params['invitation_stats'])
11
+
12
+ set_basics(params['basics']) if params['basics']
13
+ set_profile(params['profile']) if params['profile']
14
+ set_emails(params['emails']) if params['emails']
15
+ set_public_keys(params['public_keys']) if params['public_keys']
16
+ set_private_keys(params['private_keys']) if params['private_keys']
17
+ end
18
+
19
+ # Lookup a user on Keybase
20
+ #
21
+ # @param [String] username the username of the user you are searching for
22
+ # @raise [Keybase::UserNotFoundError] if the user is not found
23
+ # @raise [Keybase::InputError] if the username is empty or invalid
24
+ # @return [Keybase::Model::User] the user, if they exist
25
+ def self.lookup(username)
26
+ new(Request::User.lookup(username))
27
+ end
28
+
29
+ # Login to Keybase
30
+ #
31
+ # @param [String] email_or_username the email or username of the account
32
+ # @param [String] passphrase the passphrase for the account
33
+ # @raise [Keybase::UserNotFoundError] if the user is not found
34
+ # @raise [Keybase::InputError] if the submitted parameters are empty or invalid
35
+ # @raise [Keybase::BadPasswordError] if the submitted passphrase is incorrect
36
+ # @return [Keybase::Model::User] the user, if login is successful
37
+ def self.login(email_or_username, passphrase)
38
+ salt, login_session = Request::Root.get_salt_and_login_session(email_or_username)
39
+ pwh = Crypto.scrypt(passphrase, salt)
40
+ hmac_pwh = Crypto.hmac_sha512(pwh, login_session)
41
+ response = Request::Root.login(email_or_username, hmac_pwh, login_session)
42
+ return new(response['me'])
43
+ end
44
+
45
+ # Post a self-signed authentication certificate to Keybase
46
+ #
47
+ # This requires login first.
48
+ #
49
+ # The payload of the signature should take the form of other keybase signatures,
50
+ # like self-signing keys, or proving ownership of remote accounts.
51
+ #
52
+ # An example looks like:
53
+ #
54
+ # {
55
+ # "body": {
56
+ # "key": {
57
+ # "fingerprint": "da99a6ebeca98b14d944cb6e1ca9bfeab344f0fc",
58
+ # "host": "keybase.io",
59
+ # "key_id": "1ca9bfeab344f0fc",
60
+ # "uid": "15a9e2826313eaf005291a1ae00c3f00",
61
+ # "username": "taco422107"
62
+ # },
63
+ # "nonce": null,
64
+ # "type": "auth",
65
+ # "version": 1
66
+ # },
67
+ # "ctime": 1386537779,
68
+ # "expire_in": 86400,
69
+ # "tag": "signature"
70
+ # }
71
+ #
72
+ # The client can provide an optional nonce to randomize the signatures. The server
73
+ # will check the signature for validatity, and on success, will return an auth_token,
74
+ # which is the SHA-256 hash of the full signature body, from the "---- BEGIN" all the
75
+ # way through to the ---- END PGP MESSAGE ----.
76
+ #
77
+ # @param [String] sig the whole certificate contents
78
+ # @raise [Keybase::InputError] if the certificate is empty or invalid
79
+ # @raise [Keybase::BadSessionError] if the session is not valid
80
+ # @raise [Keybase::CSRFVerificationError] if the CSRF token is not valid
81
+ # @return [String] the authentication token
82
+ def post_auth(sig)
83
+ Request::Sig.post_auth(basics.username, sig)
84
+ end
85
+
86
+ # Add a new public key to Keybase
87
+ #
88
+ # This requires login first.
89
+ #
90
+ # @param [String] key the public key
91
+ # @raise [Keybase::InputError] if the key is empty or invalid
92
+ # @raise [Keybase::BadSessionError] if the session is not valid
93
+ # @raise [Keybase::CSRFVerificationError] if the CSRF token is not valid
94
+ # @return [String] The Key ID for the uploaded key
95
+ def add_public_key(key)
96
+ Request::Key.add(public_key: key)
97
+ end
98
+
99
+ # Add a new private key to Keybase
100
+ #
101
+ # This requires login first.
102
+ #
103
+ # @param [String] key the private key
104
+ # @raise [Keybase::InputError] if the key is empty or invalid
105
+ # @raise [Keybase::BadSessionError] if the session is not valid
106
+ # @raise [Keybase::CSRFVerificationError] if the CSRF token is not valid
107
+ # @return [String] The Key ID for the uploaded key
108
+ def add_private_key(key)
109
+ Request::Key.add(private_key: key)
110
+ end
111
+
112
+ # Revoke a key from Keybase
113
+ #
114
+ # This requires login first.
115
+ #
116
+ # Currently the key is simply deleted - full revokation is due in later
117
+ # revisions of the API.
118
+ #
119
+ # @param [String] kid the key id to be revoked
120
+ # @raise [Keybase::InputError] if the key id is empty or invalid
121
+ # @raise [Keybase::BadSessionError] if the session is not valid
122
+ # @raise [Keybase::CSRFVerificationError] if the CSRF token is not valid
123
+ # @return [Boolean] success
124
+ def revoke_key(kid)
125
+ Request::Key.revoke(kid)
126
+ end
127
+
128
+ private
129
+
130
+ def set_basics(params)
131
+ @basics = OpenStruct.new(params.merge(created_at: nil, updated_at: nil))
132
+ @basics.created_at = Time.at(@basics.ctime).to_datetime if @basics.ctime
133
+ @basics.updated_at = Time.at(@basics.mtime).to_datetime if @basics.mtime
134
+ end
135
+
136
+ def set_profile(params)
137
+ @profile = OpenStruct.new(params.merge(updated_at: nil))
138
+ @profile.updated_at = Time.at(@profile.mtime).to_datetime if @profile.mtime
139
+ end
140
+
141
+ def set_emails(params)
142
+ @emails = OpenStruct.new(params)
143
+ params.each do |k,v|
144
+ v.merge!('is_verified?' => (v['is_verified'] == 1 ? true : false))
145
+ @emails.send("#{k}=".to_sym, OpenStruct.new(v))
146
+ end
147
+ end
148
+
149
+ def set_public_keys(params)
150
+ @public_keys = OpenStruct.new(params)
151
+ params.each do |k,v|
152
+ v.merge!('created_at' => Time.at(v['ctime']).to_datetime)
153
+ v.merge!('updated_at' => Time.at(v['mtime']).to_datetime)
154
+ @public_keys.send("#{k}=".to_sym, OpenStruct.new(v))
155
+ end
156
+ end
157
+
158
+ def set_private_keys(params)
159
+ @private_keys = OpenStruct.new(params)
160
+ params.each do |k,v|
161
+ v.merge!('created_at' => Time.at(v['ctime']).to_datetime)
162
+ v.merge!('updated_at' => Time.at(v['mtime']).to_datetime)
163
+ @private_keys.send("#{k}=".to_sym, OpenStruct.new(v))
164
+ end
165
+ end
166
+
167
+ end
168
+ end
@@ -0,0 +1,32 @@
1
+ module Keybase
2
+ # @private
3
+ module Request
4
+ API_BASE_URL = 'https://keybase.io'
5
+ class Base
6
+
7
+ def self.get(url, params={})
8
+ Keybase::Response.new(conn.get(url, params)).body
9
+ end
10
+
11
+ def self.post(url, params={})
12
+ response = Keybase::Response.new(conn.post(url, params))
13
+ TokenStore.cookie = response.cookie if response.cookie
14
+ response.body
15
+ end
16
+
17
+ private
18
+
19
+ def self.conn
20
+ Faraday.new(:url => API_BASE_URL) do |faraday|
21
+ faraday.path_prefix = "/_/api/1.0"
22
+ faraday.request :url_encoded
23
+ faraday.headers['Cookie'] = TokenStore.cookie if TokenStore.cookie
24
+ faraday.headers['X-CSRF-Token'] = TokenStore.csrf if TokenStore.csrf
25
+ # faraday.response :logger
26
+ faraday.adapter Faraday.default_adapter
27
+ end
28
+ end
29
+
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,10 @@
1
+ # GET dump/all.json
2
+ module Keybase
3
+ module Request
4
+ class Dump < Base
5
+ def self.all
6
+ get('dump/all.json', {})['dumps']
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ # GET dump/latest.json
2
+ module Keybase
3
+ module Request
4
+ class Dump < Base
5
+ def self.latest
6
+ get('dump/latest.json', {})['dump']
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,13 @@
1
+ # POST /key/add.json
2
+ # public_key: "-----BEGIN PGP PUBLIC..."
3
+ # private_key: "hKRib2R5gqRwcml2gqRkY..."
4
+ # is_primary: true
5
+ module Keybase
6
+ module Request
7
+ class Key < Base
8
+ def self.add(params)
9
+ post('key/add.json', params.merge(is_primary: true))['kid']
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ # POST /key/revoke.json
2
+ # revocation_type: 0
3
+ # kid: "a140c70404a13370f7..."
4
+ module Keybase
5
+ module Request
6
+ class Key < Base
7
+ def self.revoke(kid)
8
+ post('key/revoke.json', revocation_type: 0,
9
+ csrf_token: TokenStore.csrf, kid: kid)
10
+ true
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,12 @@
1
+ # GET /getsalt.json?email_or_username=EMAIL_OR_USERNAME
2
+ module Keybase
3
+ module Request
4
+ class Root < Base
5
+ def self.get_salt_and_login_session(email_or_username)
6
+ result = get('getsalt.json', email_or_username: email_or_username)
7
+ TokenStore.csrf = result['csrf_token']
8
+ return [result['salt'], result['login_session']]
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,14 @@
1
+ # POST /login.json
2
+ # email_or_username: "chris"
3
+ # hmac_pwh: "38902fe098f..."
4
+ # login_session: "lgHZIwfee99..."
5
+ module Keybase
6
+ module Request
7
+ class Root < Base
8
+ def self.login(email_or_username, hmac_pwh, login_session)
9
+ post('login.json', email_or_username: email_or_username,
10
+ hmac_pwh: hmac_pwh, login_session: login_session)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ # POST sig/post_auth.json
2
+ # sig : "----- BEGIN PGP MESSAGE ----- ..."
3
+ # email_or_username : "maxtaco"
4
+ module Keybase
5
+ module Request
6
+ class Sig < Base
7
+ def self.post_auth(email_or_username, sig)
8
+ post('sig/post_auth.json', email_or_username: email_or_username,
9
+ sig: sig)['auth_token']
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,10 @@
1
+ # GET /user/lookup.json?username=USERNAME
2
+ module Keybase
3
+ module Request
4
+ class User < Base
5
+ def self.lookup(username)
6
+ get('user/lookup.json', username: username)['them']
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,14 @@
1
+ module Keybase
2
+ # @private
3
+ class Response
4
+
5
+ attr_reader :body, :cookie
6
+
7
+ def initialize(server_response)
8
+ @body = JSON.parse(server_response.body)
9
+ Error.raise_unless_successful(@body['status'])
10
+ @cookie = server_response.headers['Set-Cookie'] ? server_response.headers['Set-Cookie'] : nil
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,32 @@
1
+ module Keybase
2
+ # @private
3
+ class TokenStore
4
+
5
+ attr_accessor :csrf, :cookie
6
+
7
+ def self.csrf
8
+ instance.csrf
9
+ end
10
+
11
+ def self.csrf=(csrf)
12
+ instance.csrf = csrf
13
+ end
14
+
15
+ def self.cookie
16
+ instance.cookie
17
+ end
18
+
19
+ def self.cookie=(cookie)
20
+ instance.cookie = cookie
21
+ end
22
+
23
+ def self.instance
24
+ @@instance ||= new
25
+ end
26
+
27
+ private
28
+
29
+ def initialize ; end
30
+
31
+ end
32
+ end
@@ -0,0 +1,6 @@
1
+ dir = File.dirname(__FILE__)
2
+ $LOAD_PATH.unshift(dir)
3
+
4
+ Dir["#{dir}/**/*_test.rb"].sort.each do |file|
5
+ require file.sub(/^#{dir}\/(.*)\.rb$/, '\1')
6
+ end
@@ -0,0 +1,9 @@
1
+ {
2
+ "dump_id": "472eba4586993583b395b00e",
3
+ "full_data": "https://s3.amazonaws.com/keybase_data_dumps/2014-02-24-13-46-31-...json",
4
+ "full_data_sha256": "bea8863f1b17d35d7db4480197886e2f2d9f8deb0633c5f2adcc427541b79bd7",
5
+ "parent_dump_id": "c56698ea60027c2e9f98620e",
6
+ "changes_from_parent": "https://s3.amazonaws.com/keybase_data_dumps/2014-02-...json",
7
+ "changes_sha256": "88a248688299eb2fd25d1031d35abca8243ad5100fc614a213d300512fd2b634",
8
+ "ctime": 1393267591
9
+ }
@@ -0,0 +1,50 @@
1
+ {
2
+ "id": "bf5f1650fc1361a",
3
+ "basics": {
4
+ "uid": "55c079bf5f1650fc1361a1c3a4709900",
5
+ "salt": "533a6842479b5e925856d1ce2a444...",
6
+ "username": "chris",
7
+ "ctime": 1383161061,
8
+ "mtime": 1383162061
9
+ },
10
+ "invitation_stats": {
11
+ "available": 3,
12
+ "used": 2,
13
+ "power": 1,
14
+ "open": 0
15
+ },
16
+ "profile": {
17
+ "full_name": "Chris Coyne",
18
+ "location": null,
19
+ "bio": "I am the terror that flaps in the night.",
20
+ "mtime": 1383247339
21
+ },
22
+ "emails": {
23
+ "primary": {
24
+ "email": "chris@okcupid.com",
25
+ "is_verified": 1
26
+ },
27
+ "secondary": {
28
+ "email": "chris@theinter.net",
29
+ "is_verified": 0
30
+ }
31
+ },
32
+ "public_keys": {
33
+ "primary": {
34
+ "kid": "d028ac1542b24a5f77f123ba467873e3ce96a992570a",
35
+ "key_type": 1,
36
+ "bundle": "-----BEGIN PGP PUBLIC KEY BLOCK-----\\nVersion => Keybase OpenPGP JS 0.0.1\\nComment => https => //keybase.io\\n\\nxo0EUnlbBAEEALU9OLTEIA0h2o8NZc+0b9f8RaCa8nLfqm3cKNZ3aAV9iYLkeHnd\\nqhP99c+VTiB3Rtjeqf9hZAqKCIJlVrURPgN/mPRzRk5u4OmI/vw/HMn+3lBLwTJZ\\n+FQGEGvLvzBz9wEpcM3IKrXl70BJbq/uM3SP0fsu60/fIy4twEvYOS9JABEBAAHN\\nI2tleWJhc2UuaW8vY2hyaXMgPGNocmlzQGtleWJhc2UuaW8+wrMEEAEKAB0FAlJ5\\nWwQCGy8FCRLMAwADCwkHAxUKCAIeAQIXgAAKCRAb5d5e+VB/cndDA/0Rgsu84CpW\\nN2lwvjCr/MJM9MV8Y75NAl5SFArKksg1d4DYH40M0boJuHETk3AR60bHDRlrhS5/\\nUPm9+A3PAa4IRXKw967e/8nhH3o8mhstDRCMJUrNfo2so5JzFSgWQaflDta0kE/E\\nNlUEOBqmt9Y2f/elhuxiGa0GkoBpJja2ks6NBFJ5WwQBBACva+Om+XqIrBMyDO54\\nNptSiNnjEEJoFJ6hMCqR3S88Kj/Jzu3jTiY+sqOsCDqMOv83AiKWBEL7U28eqhb7\\nwUEOL9nbKOWsQVvCqtla8vgQBLPZ3kYxs1CfJCnaC/bT6abh8+OHECSTENmurSV0\\na22IlMNJrU+3DZ/A3nrca8GGcQARAQABwsCDBBgBCgAPBQJSeVsEBQkB4TOAAhsu\\nAKgJEBvl3l75UH9ynSAEGQEKAAYFAlJ5WwQACgkQl6sBE/Y073xyYwQAryX0fcCA\\nO/ZCZi3pC26CWUtu6Vr6uSl9VSIRQFqcNoZ7PIcjQwGif/iHbBSap1wDkRBlMKkd\\n76vJi67z3etGZ5bJUb4iG/IJUOx6Yn2RGFtyV6N3t1w84dafqaR3cuOsT+UPYSIB\\njxgUPqfN68hqhW0ZBjXCva/P3dPhLulm6zY1AgQAqNDxGPui5OfNBzczq2raoF8E\\nyVJ1gRtP1eGJv6LZ01ABCSJg4h+tgKZuVHIX+i+rcqOY3ABEYWrhfjJBuVNPkM2I\\nFoMllSH7/ywNv8k8PvNBtybqrMiIUJyGrtLvjKhxmD+yxxvPcHWbTkearCycY4QW\\nVlLXYoBhF0AWKph5v/M=\\n=3FfV\\n-----END PGP PUBLIC KEY BLOCK-----\\n",
37
+ "mtime": 1383685212,
38
+ "ctime": 1383685212
39
+ }
40
+ },
41
+ "private_keys": {
42
+ "primary": {
43
+ "kid": "d028ac1542b24a5f77f123ba467873e3ce96a992570a",
44
+ "key_type": 2,
45
+ "bundle": "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",
46
+ "mtime": 1383685212,
47
+ "ctime": 1383685212
48
+ }
49
+ }
50
+ }
@@ -0,0 +1,23 @@
1
+ require_relative '../test_helper'
2
+ require_relative '../integration_test_helper'
3
+
4
+ module Keybase
5
+ class DumpsIntegrationTest < Minitest::Test
6
+
7
+ def setup
8
+ VCR.use_cassette('dumps') do
9
+ @all = Keybase.dump_all
10
+ @latest = Keybase.dump_latest
11
+ end
12
+ end
13
+
14
+ def test_latest_dump_object_has_expected_fields
15
+ assert @latest.respond_to? :id
16
+ end
17
+
18
+ def test_first_dump_object_has_expected_fields
19
+ assert @all[0].respond_to? :id
20
+ end
21
+
22
+ end
23
+ end