Skip to content

Commit 601917f

Browse files
committed
Move encryption to LanguageAuthClient and encrypt profiles in-transit
1 parent 255214c commit 601917f

File tree

2 files changed

+81
-66
lines changed

2 files changed

+81
-66
lines changed

packages/core/src/auth/auth2.ts

Lines changed: 55 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import {
1919
invalidateSsoTokenRequestType,
2020
invalidateStsCredentialRequestType,
2121
ProfileKind,
22-
UpdateProfileParams,
2322
updateProfileRequestType,
2423
SsoTokenChangedParams,
2524
StsCredentialChangedParams,
@@ -50,8 +49,8 @@ import {
5049
Profile,
5150
SsoSession,
5251
GetMfaCodeParams,
53-
GetMfaCodeResult,
5452
getMfaCodeRequestType,
53+
GetMfaCodeResult,
5554
} from '@aws/language-server-runtimes/protocol'
5655
import { LanguageClient } from 'vscode-languageclient'
5756
import { getLogger } from '../shared/logger/logger'
@@ -125,12 +124,36 @@ export class LanguageClientAuth {
125124
return this.#ssoCacheWatcher
126125
}
127126

128-
getSsoToken(
127+
/**
128+
* Encrypts an object
129+
*/
130+
private async encrypt<T>(request: T): Promise<string> {
131+
const payload = new TextEncoder().encode(JSON.stringify(request))
132+
const encrypted = await new jose.CompactEncrypt(payload)
133+
.setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })
134+
.encrypt(this.encryptionKey)
135+
return encrypted
136+
}
137+
138+
/**
139+
* Decrypts an object
140+
*/
141+
private async decrypt<T>(request: string): Promise<T> {
142+
try {
143+
const result = await jose.compactDecrypt(request, this.encryptionKey)
144+
return JSON.parse(new TextDecoder().decode(result.plaintext)) as T
145+
} catch (e) {
146+
getLogger().error(`Failed to decrypt: ${request}`)
147+
return request as T
148+
}
149+
}
150+
151+
async getSsoToken(
129152
tokenSource: TokenSource,
130153
login: boolean = false,
131154
cancellationToken?: CancellationToken
132155
): Promise<GetSsoTokenResult> {
133-
return this.client.sendRequest(
156+
const response: GetSsoTokenResult = await this.client.sendRequest(
134157
getSsoTokenRequestType.method,
135158
{
136159
clientName: this.clientName,
@@ -142,14 +165,17 @@ export class LanguageClientAuth {
142165
} satisfies GetSsoTokenParams,
143166
cancellationToken
144167
)
168+
// Decrypt the access token
169+
response.ssoToken.accessToken = await this.decrypt(response.ssoToken.accessToken)
170+
return response
145171
}
146172

147-
getIamCredential(
173+
async getIamCredential(
148174
profileName: string,
149175
login: boolean = false,
150176
cancellationToken?: CancellationToken
151177
): Promise<GetIamCredentialResult> {
152-
return this.client.sendRequest(
178+
const response: GetIamCredentialResult = await this.client.sendRequest(
153179
getIamCredentialRequestType.method,
154180
{
155181
profileName: profileName,
@@ -159,16 +185,25 @@ export class LanguageClientAuth {
159185
} satisfies GetIamCredentialParams,
160186
cancellationToken
161187
)
188+
// Decrypt the response credentials
189+
const { accessKeyId, secretAccessKey, sessionToken, expiration } = response.credential.credentials
190+
response.credential.credentials = {
191+
accessKeyId: await this.decrypt(accessKeyId),
192+
secretAccessKey: await this.decrypt(secretAccessKey),
193+
sessionToken: sessionToken ? await this.decrypt(sessionToken) : undefined,
194+
expiration: expiration,
195+
}
196+
return response
162197
}
163198

164-
updateSsoProfile(
199+
async updateSsoProfile(
165200
profileName: string,
166201
startUrl: string,
167202
region: string,
168203
scopes: string[]
169204
): Promise<UpdateProfileResult> {
170205
// Add SSO settings and delete credentials from profile
171-
return this.client.sendRequest(updateProfileRequestType.method, {
206+
const params = await this.encrypt({
172207
profile: {
173208
kinds: [ProfileKind.SsoTokenProfile],
174209
name: profileName,
@@ -188,10 +223,11 @@ export class LanguageClientAuth {
188223
sso_registration_scopes: scopes,
189224
},
190225
},
191-
} satisfies UpdateProfileParams)
226+
})
227+
return this.client.sendRequest(updateProfileRequestType.method, params)
192228
}
193229

194-
updateIamProfile(profileName: string, opts: IamProfileOptions): Promise<UpdateProfileResult> {
230+
async updateIamProfile(profileName: string, opts: IamProfileOptions): Promise<UpdateProfileResult> {
195231
// Substitute missing fields for defaults
196232
const fields = { ...IamProfileOptionsDefaults, ...opts }
197233
// Get the profile kind matching the provided fields
@@ -204,7 +240,7 @@ export class LanguageClientAuth {
204240
kind = ProfileKind.Unknown
205241
}
206242

207-
return this.client.sendRequest(updateProfileRequestType.method, {
243+
const params = await this.encrypt({
208244
profile: {
209245
kinds: [kind],
210246
name: profileName,
@@ -217,10 +253,12 @@ export class LanguageClientAuth {
217253
},
218254
},
219255
})
256+
return this.client.sendRequest(updateProfileRequestType.method, params)
220257
}
221258

222-
listProfiles() {
223-
return this.client.sendRequest(listProfilesRequestType.method, {}) as Promise<ListProfilesResult>
259+
async listProfiles() {
260+
const response: string = await this.client.sendRequest(listProfilesRequestType.method, {})
261+
return await this.decrypt<ListProfilesResult>(response)
224262
}
225263

226264
/**
@@ -352,19 +390,6 @@ export abstract class BaseLogin {
352390
this.eventEmitter.fire({ id: this.profileName, state: this.connectionState })
353391
}
354392
}
355-
356-
/**
357-
* Decrypts an encrypted string, removes its quotes, and returns the resulting string
358-
*/
359-
protected async decrypt(encrypted: string): Promise<string> {
360-
try {
361-
const decrypted = await jose.compactDecrypt(encrypted, this.lspAuth.encryptionKey)
362-
return decrypted.plaintext.toString().replaceAll('"', '')
363-
} catch (e) {
364-
getLogger().error(`Failed to decrypt: ${encrypted}`)
365-
return encrypted
366-
}
367-
}
368393
}
369394

370395
/**
@@ -436,9 +461,8 @@ export class SsoLogin extends BaseLogin {
436461
*/
437462
async getCredential() {
438463
const response = await this._getSsoToken(false)
439-
const accessToken = await this.decrypt(response.ssoToken.accessToken)
440464
return {
441-
credential: accessToken,
465+
credential: response.ssoToken.accessToken,
442466
updateCredentialsParams: response.updateCredentialsParams,
443467
}
444468
}
@@ -562,7 +586,7 @@ export class IamLogin extends BaseLogin {
562586
sourceProfile: sourceProfile,
563587
})
564588
} else {
565-
// Create the target profile
589+
// Create the credentials profile
566590
await this.lspAuth.updateIamProfile(this.profileName, {
567591
accessKey: opts.accessKey,
568592
secretKey: opts.secretKey,
@@ -575,14 +599,6 @@ export class IamLogin extends BaseLogin {
575599
* Restore the connection state and connection details to memory, if they exist.
576600
*/
577601
async restore() {
578-
const sessionData = await this.getProfile()
579-
const credentials = sessionData?.profile?.settings
580-
if (credentials?.aws_access_key_id && credentials?.aws_secret_access_key) {
581-
this._data = {
582-
accessKey: credentials.aws_access_key_id,
583-
secretKey: credentials.aws_secret_access_key,
584-
}
585-
}
586602
try {
587603
await this._getIamCredential(false)
588604
} catch (err) {
@@ -596,15 +612,8 @@ export class IamLogin extends BaseLogin {
596612
*/
597613
async getCredential() {
598614
const response = await this._getIamCredential(false)
599-
const credentials: IamCredentials = {
600-
accessKeyId: await this.decrypt(response.credential.credentials.accessKeyId),
601-
secretAccessKey: await this.decrypt(response.credential.credentials.secretAccessKey),
602-
sessionToken: response.credential.credentials.sessionToken
603-
? await this.decrypt(response.credential.credentials.sessionToken)
604-
: undefined,
605-
}
606615
return {
607-
credential: credentials,
616+
credential: response.credential.credentials,
608617
updateCredentialsParams: response.updateCredentialsParams,
609618
}
610619
}
@@ -639,7 +648,7 @@ export class IamLogin extends BaseLogin {
639648
}
640649

641650
// Update cached credentials and credential ID
642-
if (response.credential?.credentials?.accessKeyId && response.credential?.credentials?.secretAccessKey) {
651+
if (response.credential.credentials.accessKeyId && response.credential.credentials.secretAccessKey) {
643652
this._data = {
644653
accessKey: response.credential.credentials.accessKeyId,
645654
secretKey: response.credential.credentials.secretAccessKey,

packages/core/src/test/credentials/auth2.test.ts

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ const tokenId = 'test-token'
4040
describe('LanguageClientAuth', () => {
4141
let client: sinon.SinonStubbedInstance<LanguageClient>
4242
let auth: LanguageClientAuth
43-
const encryptionKey = Buffer.from('test-key')
43+
const encryptionKey = Buffer.from('test-key'.padEnd(32, '0'))
4444
let useDeviceFlowStub: sinon.SinonStub
4545

4646
beforeEach(() => {
@@ -61,6 +61,16 @@ describe('LanguageClientAuth', () => {
6161
}
6262
useDeviceFlowStub.returns(useDeviceFlow ? true : false)
6363

64+
client.sendRequest.resolves({
65+
ssoToken: {
66+
id: 'my-id',
67+
accessToken: 'my-access-token',
68+
},
69+
updateCredentialsParams: {
70+
data: 'my-data',
71+
},
72+
} satisfies GetSsoTokenResult)
73+
6474
await auth.getSsoToken(tokenSource, true)
6575

6676
sinon.assert.calledOnce(client.sendRequest)
@@ -94,13 +104,6 @@ describe('LanguageClientAuth', () => {
94104
await auth.updateSsoProfile(profileName, startUrl, region, ['scope1'])
95105

96106
sinon.assert.calledOnce(client.sendRequest)
97-
const requestParams = client.sendRequest.firstCall.args[1]
98-
sinon.assert.match(requestParams.profile, {
99-
name: profileName,
100-
})
101-
sinon.assert.match(requestParams.ssoSession.settings, {
102-
sso_region: region,
103-
})
104107
})
105108

106109
it('sends correct IAM profile update parameters', async () => {
@@ -111,18 +114,6 @@ describe('LanguageClientAuth', () => {
111114
})
112115

113116
sinon.assert.calledOnce(client.sendRequest)
114-
const requestParams = client.sendRequest.firstCall.args[1]
115-
sinon.assert.match(requestParams.profile, {
116-
name: profileName,
117-
kinds: [ProfileKind.IamCredentialsProfile],
118-
})
119-
sinon.assert.match(requestParams.profile.settings, {
120-
aws_access_key_id: 'myAccessKey',
121-
aws_secret_access_key: 'mySecretKey',
122-
aws_session_token: 'mySessionToken',
123-
role_arn: '',
124-
source_profile: '',
125-
})
126117
})
127118
})
128119

@@ -213,6 +204,21 @@ describe('LanguageClientAuth', () => {
213204

214205
describe('getIamCredential', () => {
215206
it('sends correct request parameters', async () => {
207+
client.sendRequest.resolves({
208+
credential: {
209+
id: 'my-id',
210+
kinds: [],
211+
credentials: {
212+
accessKeyId: 'my-access-key',
213+
secretAccessKey: 'my-secret-key',
214+
sessionToken: 'my-session-token',
215+
},
216+
},
217+
updateCredentialsParams: {
218+
data: 'my-data',
219+
},
220+
} satisfies GetIamCredentialResult)
221+
216222
await auth.getIamCredential(profileName, true)
217223

218224
sinon.assert.calledOnce(client.sendRequest)

0 commit comments

Comments
 (0)