Skip to content

Commit 7ac37a6

Browse files
authored
Merge pull request #186 from detunized/issue-184
1Password: correctly detect invalid vault and item ID
2 parents 37bcf40 + b966b6d commit 7ac37a6

File tree

6 files changed

+181
-39
lines changed

6 files changed

+181
-39
lines changed

src/OnePassword/Client.cs

Lines changed: 56 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -79,27 +79,50 @@ public static Vault OpenVault(VaultInfo info, Session session)
7979

8080
public static OneOf<Account, SshKey, NoItem> GetItem(string itemId, string vaultId, Session session)
8181
{
82-
var oneOf3 = GetVaultItem(itemId, vaultId, session.Keychain, session.Key, session.Rest);
82+
// 1. On the first request we fetch everything we need to decrypt the item. This is also allows us to check
83+
// upfront if the vault exists and if it's accessible.
84+
if (session.AccountInfo == null)
85+
{
86+
var accountInfo = GetAccountInfo(session.Key, session.Rest);
87+
var keysetsInfo = GetKeysets(session.Key, session.Rest);
8388

84-
// The item is not available
85-
if (oneOf3.TryPickT2(out var failure, out var oneOf2))
86-
return failure;
89+
// Decrypt into the session keychain
90+
DecryptKeysets(keysetsInfo.Keysets, session.Credentials, session.Keychain);
8791

88-
var item = oneOf2.Match<VaultItem>(a => a, k => k);
92+
// Figure out which vaults are accessible with the current keychain
93+
var accessibleVaults = GetAccessibleVaults(accountInfo, session.Keychain).ToArray();
8994

90-
if (CanDecrypt(item))
91-
return oneOf3;
95+
// Decrypt all the vault keys into the session keychain
96+
foreach (var v in accessibleVaults)
97+
v.DecryptKeyIntoKeychain();
98+
99+
// Store last to ensure consistent state
100+
session.AccountInfo = accountInfo;
101+
session.AccessibleVaults = accessibleVaults;
102+
}
103+
104+
// 2. Check if the vault ID is valid and the vault exists
105+
if (!session.AccountInfo.Vaults.Any(x => x.Id == vaultId))
106+
return NoItem.NotFound;
107+
108+
// 3. Even if the vault is there, it might not be accessible
109+
if (!session.AccessibleVaults.Any(x => x.Id == vaultId))
110+
return NoItem.Inaccessible;
111+
112+
// 4. Download the item
113+
var oneOf3 = GetVaultItem(itemId, vaultId, session.Keychain, session.Key, session.Rest);
114+
115+
// 5. Check if the item is not available
116+
if (oneOf3.TryPickT2(out var noItem, out var oneOf2))
117+
return noItem;
92118

93-
// Attempt to fetch everything necessary to decrypt the item
94-
var accountInfo = GetAccountInfo(session.Key, session.Rest);
95-
var keysets = GetKeysets(session.Key, session.Rest);
96-
DecryptKeysets(keysets.Keysets, session.Credentials, session.Keychain);
97-
GetAccessibleVaults(accountInfo, session.Keychain).FirstOrDefault(x => x.Id == vaultId)?.DecryptKeyIntoKeychain();
119+
// 6. It's either an account or a SSH key
120+
var item = oneOf2.Match<VaultItem>(a => a, k => k);
98121

99122
if (CanDecrypt(item))
100123
return oneOf3;
101124

102-
throw new InternalErrorException("Failed to fetch the keys to decrypt the item");
125+
return NoItem.Inaccessible;
103126

104127
bool CanDecrypt(VaultItem i) => session.Keychain.CanDecrypt(i.EncryptedOverview) && session.Keychain.CanDecrypt(i.EncryptedDetails);
105128
}
@@ -647,6 +670,7 @@ internal static string SubmitSecondFactorResult(SecondFactorKind factor, SecondF
647670
}
648671
}
649672

673+
// TODO: Rename to RequestAccountInfo
650674
internal static R.AccountInfo GetAccountInfo(AesKey sessionKey, RestClient rest)
651675
{
652676
return GetEncryptedJson<R.AccountInfo>(
@@ -656,11 +680,13 @@ internal static R.AccountInfo GetAccountInfo(AesKey sessionKey, RestClient rest)
656680
);
657681
}
658682

683+
// TODO: Rename to RequestKeysets
659684
internal static R.KeysetsInfo GetKeysets(AesKey sessionKey, RestClient rest)
660685
{
661686
return GetEncryptedJson<R.KeysetsInfo>("v1/account/keysets", sessionKey, rest);
662687
}
663688

689+
// TODO: We don't really need IEnumerable here
664690
internal static IEnumerable<VaultInfo> GetAccessibleVaults(R.AccountInfo accountInfo, Keychain keychain)
665691
{
666692
return from vault in accountInfo.Vaults
@@ -736,15 +762,18 @@ internal static OneOf<Account, SshKey, NoItem> GetVaultItem(
736762
RestClient rest
737763
)
738764
{
765+
// TODO: Make a request to var response = rest.Get<R.Encrypted>($"v1/vault/{vaultId}/"); to check if the vault exists!
739766
var response = rest.Get<R.Encrypted>($"v1/vault/{vaultId}/item/{itemId}");
740767
if (response.IsSuccessful)
741768
return ConvertVaultItem(keychain, DecryptResponse<R.SingleVaultItem>(response.Data, sessionKey).Item);
742769

743-
// Special case: the item not found
744-
if (response.StatusCode == System.Net.HttpStatusCode.BadRequest && response.Content.Trim() == "{}")
745-
return NoItem.NotFound;
746-
747-
throw MakeError(response);
770+
var error = MakeError(response);
771+
return error switch
772+
{
773+
// Special case: the item not found
774+
NotFoundException => NoItem.NotFound,
775+
_ => throw error,
776+
};
748777
}
749778

750779
// TODO: Rename to RequestVaultAccounts? It should clearer from the name that it's a slow operation.
@@ -844,9 +873,11 @@ internal static AesKey DeriveMasterKey(string algorithm, int iterations, byte[]
844873
}
845874

846875
//
847-
// HTTP
876+
// Error handling
848877
//
849878

879+
internal class NotFoundException(string message) : BaseException(message);
880+
850881
internal static BaseException MakeError(RestResponse<string> response)
851882
{
852883
if (response.IsNetworkError)
@@ -869,6 +900,8 @@ internal static BaseException ParseServerError(string response)
869900
{
870901
case 102:
871902
return new BadCredentialsException("Username, password or account key is incorrect");
903+
case 117:
904+
return new NotFoundException($"The requested item not found: '{error.Message}'");
872905
default:
873906
return new InternalErrorException($"The server responded with the error code {error.Code} and the message '{error.Message}'");
874907
}
@@ -892,6 +925,10 @@ internal static BaseException ParseServerError(string response)
892925
return null;
893926
}
894927

928+
//
929+
// Network
930+
//
931+
895932
internal static T GetEncryptedJson<T>(string endpoint, AesKey sessionKey, RestClient rest)
896933
{
897934
var response = rest.Get<R.Encrypted>(endpoint);

src/OnePassword/NoItem.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ public enum NoItem
88
NotFound,
99
Deleted,
1010
UnsupportedType,
11+
Inaccessible,
1112
}

src/OnePassword/Session.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
// Copyright (C) Dmitry Yakimenko (detunized@gmail.com).
22
// Licensed under the terms of the MIT license. See LICENCE for details.
33

4+
#nullable enable
5+
46
using PasswordManagerAccess.Common;
7+
using R = PasswordManagerAccess.OnePassword.Response;
58

69
namespace PasswordManagerAccess.OnePassword
710
{
@@ -15,6 +18,10 @@ public class Session
1518
internal readonly RestClient Rest;
1619
internal readonly IRestTransport Transport;
1720

21+
// Cache
22+
internal R.AccountInfo? AccountInfo { get; set; }
23+
internal VaultInfo[]? AccessibleVaults { get; set; }
24+
1825
internal Session(Credentials credentials, Keychain keychain, AesKey key, RestClient rest, IRestTransport transport)
1926
{
2027
Credentials = credentials;

test/OnePassword/ClientTest.cs

Lines changed: 84 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,83 @@ namespace PasswordManagerAccess.Test.OnePassword
2424
{
2525
public class ClientTest : TestBase
2626
{
27+
//
28+
// Public methods
29+
//
30+
31+
[Fact]
32+
public void GetItem_returns_account()
33+
{
34+
// Arrange
35+
var transport = new RestFlow()
36+
.Get(EncryptFixture("get-vault-item-response"))
37+
.ExpectUrl("1password.com/api/v1/vault/ru74fjxlkipzzctorwj4icrj2a/item/wm3uxq4xsmb4mghxw6o3s7zrem");
38+
var rest = transport.ToRestClient(ApiUrl);
39+
var keychain = new Keychain(
40+
new AesKey("x4ouqoqyhcnqojrgubso4hsdga", "ce92c6d1af345c645211ad49692b22338d128d974e3b6718c868e02776c873a9".DecodeHex())
41+
);
42+
var accountInfo = JsonConvert.DeserializeObject<R.AccountInfo>(GetFixture("get-account-info-response"));
43+
var vault = accountInfo.Vaults.First(x => x.Id == "ru74fjxlkipzzctorwj4icrj2a");
44+
var vaultInfo = new VaultInfo(vault.Id, Encrypted.Parse(vault.Attributes), Encrypted.Parse(vault.Access[0].EncryptedKey), keychain);
45+
var session = new Session(TestData.Credentials, keychain, TestData.SessionKey, rest, transport)
46+
{
47+
AccountInfo = accountInfo,
48+
AccessibleVaults = [vaultInfo],
49+
};
50+
51+
// Act
52+
var result = Client.GetItem("wm3uxq4xsmb4mghxw6o3s7zrem", "ru74fjxlkipzzctorwj4icrj2a", session);
53+
54+
// Assert
55+
result.TryPickT0(out var account, out _).ShouldBeTrue();
56+
account.Id.ShouldBe("wm3uxq4xsmb4mghxw6o3s7zrem");
57+
}
58+
59+
[Fact]
60+
public void GetItem_returns_NoItem_when_not_found()
61+
{
62+
// Arrange
63+
var transport = new RestFlow()
64+
.Get(GetFixture("get-vault-item-not-found-response"), HttpStatusCode.NotFound)
65+
.ExpectUrl("1password.com/api/v1/vault/ru74fjxlkipzzctorwj4icrj2a/item/nonexistent");
66+
var rest = transport.ToRestClient(ApiUrl);
67+
var keychain = new Keychain(
68+
new AesKey("x4ouqoqyhcnqojrgubso4hsdga", "ce92c6d1af345c645211ad49692b22338d128d974e3b6718c868e02776c873a9".DecodeHex())
69+
);
70+
var accountInfo = JsonConvert.DeserializeObject<R.AccountInfo>(GetFixture("get-account-info-response"));
71+
var vault = accountInfo.Vaults.First(x => x.Id == "ru74fjxlkipzzctorwj4icrj2a");
72+
var vaultInfo = new VaultInfo(vault.Id, Encrypted.Parse(vault.Attributes), Encrypted.Parse(vault.Access[0].EncryptedKey), keychain);
73+
var session = new Session(TestData.Credentials, keychain, TestData.SessionKey, rest, transport)
74+
{
75+
AccountInfo = accountInfo,
76+
AccessibleVaults = [vaultInfo],
77+
};
78+
79+
// Act
80+
var result = Client.GetItem("nonexistent", "ru74fjxlkipzzctorwj4icrj2a", session);
81+
82+
// Assert
83+
result.TryPickT2(out var noItem, out _).ShouldBeTrue();
84+
noItem.ShouldBe(NoItem.NotFound);
85+
}
86+
87+
[Fact]
88+
public void ListAllVaults_returns_vaults()
89+
{
90+
// Arrange
91+
var flow = new RestFlow().Get(EncryptFixture("get-account-info-response")).Get(EncryptFixture("get-keysets-response"));
92+
93+
// Act
94+
var vaults = Client.ListAllVaults(Credentials, new Keychain(), TestData.SessionKey, flow);
95+
96+
// Assert
97+
vaults.ShouldNotBeEmpty();
98+
}
99+
100+
//
101+
// Internal methods
102+
//
103+
27104
[Fact]
28105
public void ParseServiceAccountToken_returns_parsed_token()
29106
{
@@ -56,19 +133,6 @@ public void ParseServiceAccountToken_throws_on_invalid_token(string token)
56133
ex.Message.ShouldStartWith("Invalid service account token");
57134
}
58135

59-
[Fact]
60-
public void ListAllVaults_returns_vaults()
61-
{
62-
// Arrange
63-
var flow = new RestFlow().Get(EncryptFixture("get-account-info-response")).Get(EncryptFixture("get-keysets-response"));
64-
65-
// Act
66-
var vaults = Client.ListAllVaults(Credentials, new Keychain(), TestData.SessionKey, flow);
67-
68-
// Assert
69-
vaults.ShouldNotBeEmpty();
70-
}
71-
72136
[Fact]
73137
public void StartNewSession_returns_session_on_ok()
74138
{
@@ -479,7 +543,7 @@ public void GetKeysets_makes_GET_request_to_specific_url()
479543
}
480544

481545
[Fact]
482-
public void GetVaultAccounts_returns_accounts()
546+
public void GetVaultItems_returns_accounts()
483547
{
484548
// Arrange
485549
var flow = new RestFlow().Get(EncryptFixture("get-vault-accounts-ru74-response"));
@@ -495,7 +559,7 @@ public void GetVaultAccounts_returns_accounts()
495559
}
496560

497561
[Fact]
498-
public void GetVaultAccounts_returns_ssh_keys()
562+
public void GetVaultItems_returns_ssh_keys()
499563
{
500564
// Arrange
501565
var flow = new RestFlow().Get(EncryptFixture("get-vault-accounts-ixsi-response"));
@@ -520,7 +584,7 @@ public void GetVaultAccounts_returns_ssh_keys()
520584
}
521585

522586
[Fact]
523-
public void GetVaultAccounts_returns_converts_ssh_keys()
587+
public void GetVaultItems_returns_converts_ssh_keys()
524588
{
525589
// Arrange
526590
var flow = new RestFlow().Get(EncryptFixture("get-vault-accounts-saiw-response"));
@@ -632,7 +696,7 @@ public void GetVaultAccounts_returns_converts_ssh_keys()
632696
}
633697

634698
[Fact]
635-
public void GetVaultAccounts_with_no_items_work()
699+
public void GetVaultItems_with_no_items_work()
636700
{
637701
// Arrange
638702
var flow = new RestFlow().Get(EncryptFixture("get-vault-with-no-items-response"));
@@ -648,7 +712,7 @@ public void GetVaultAccounts_with_no_items_work()
648712
}
649713

650714
[Fact]
651-
public void GetVaultAccounts_returns_server_secrets()
715+
public void GetVaultItems_returns_server_secrets()
652716
{
653717
// Arrange
654718
var flow = new RestFlow().Get(EncryptFixture("get-vault-with-server-secrets-response"));
@@ -664,7 +728,7 @@ public void GetVaultAccounts_returns_server_secrets()
664728
}
665729

666730
[Fact]
667-
public void GetVaultAccounts_makes_GET_request_to_specific_url()
731+
public void GetVaultItems_makes_GET_request_to_specific_url()
668732
{
669733
// Arrange
670734
var flow = new RestFlow()
@@ -680,7 +744,7 @@ public void GetVaultAccounts_makes_GET_request_to_specific_url()
680744
}
681745

682746
[Fact]
683-
public void GetVaultAccounts_with_multiple_batches_returns_all_accounts()
747+
public void GetVaultItems_with_multiple_batches_returns_all_accounts()
684748
{
685749
// Arrange
686750
var flow = new RestFlow()
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"errorCode": 117,
3+
"errorMessage": "Not found.",
4+
"requestId": 20988701
5+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"contentVersion": 3,
3+
"item": {
4+
"uuid": "wm3uxq4xsmb4mghxw6o3s7zrem",
5+
"templateUuid": "001",
6+
"trashed": "N",
7+
"createdAt": "2016-08-04T13:15:10Z",
8+
"updatedAt": "2016-08-04T13:16:07Z",
9+
"changerUuid": "DFM4DHQMQ5AS5M6UHSD6PNV774",
10+
"packageUuid": "",
11+
"itemVersion": 2,
12+
"encryptedBy": "x4ouqoqyhcnqojrgubso4hsdga",
13+
"encOverview": {
14+
"cty": "b5+jwk+json",
15+
"data": "9eH5PkEAnOA5QDds7BrUuwL3vo2abF1SSzwwVXwjnA_4zacK-qBCoK7flbzG-Pi_PIa9uxk3XkiJ4QavGD3FBJpdW9xxcvS5jadnyhmOfUNt7AKPLcg96hh0SLX9GdMnOGNXW_SW_UdBV4N-ux9wHv3dEf6gUw5emAYmucDcyGt4bvxNtXeFTFnZhuAB-lGA9l5xzjc",
16+
"enc": "A256GCM",
17+
"iv": "tgpqo7Cz06z2ka-W",
18+
"kid": "x4ouqoqyhcnqojrgubso4hsdga"
19+
},
20+
"encDetails": {
21+
"cty": "b5+jwk+json",
22+
"data": "d-EtBOtn5hJWRX-IRVZc03axWv_EMDZtpCgFJEzP0qAHoCv8JTZJ00oWWDCINR2zFoHg5pks8ZvEMV1nquPwmN2hQxrB9mS8nK5wfWylgKms56VsdwD3P8SIZZcUOywX79oVN7VEu5C3fqBdeAiVvG_76ziHPFryApSbk9I34dIow6uZ0ldxGd5RPVaYRPkkZ1tf3_Nmgnm2n11JxEf0fSgj9Bc554hIMWLn7x3xYJbBgot_fYHqGP0UG7oBG0mXym1Lm_6lZFopQBlgIB0",
23+
"enc": "A256GCM",
24+
"iv": "RY7AOcB6_bTUwEwN",
25+
"kid": "x4ouqoqyhcnqojrgubso4hsdga"
26+
}
27+
}
28+
}

0 commit comments

Comments
 (0)