Skip to content

Commit 99568a7

Browse files
authored
fix: cherry-pick decryption error across projects (#14582)
* fix: cherry-pick decryption error across projects * fix: add ut
1 parent 99ad0f0 commit 99568a7

File tree

2 files changed

+148
-4
lines changed

2 files changed

+148
-4
lines changed

packages/fx-core/src/core/crypto.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,32 @@ import Cryptr from "cryptr";
66

77
export class LocalCrypto implements CryptoProvider {
88
private cryptr: Cryptr;
9+
private fixedCryptr: Cryptr;
910
private prefix = "crypto_";
1011

1112
constructor(projectId: string) {
1213
this.cryptr = new Cryptr(projectId + "_teamsfx");
14+
this.fixedCryptr = new Cryptr("teamsfx_global_key");
1315
}
1416

1517
public encrypt(plaintext: string): Result<string, FxError> {
16-
return ok(this.prefix + this.cryptr.encrypt(plaintext));
18+
return ok(this.prefix + this.fixedCryptr.encrypt(plaintext));
1719
}
1820

1921
public decrypt(ciphertext: string): Result<string, FxError> {
2022
if (!ciphertext.startsWith(this.prefix)) {
2123
// legacy raw secret string
2224
return ok(ciphertext);
2325
}
26+
const encryptedData = ciphertext.substr(this.prefix.length);
2427
try {
25-
return ok(this.cryptr.decrypt(ciphertext.substr(this.prefix.length)));
28+
return ok(this.fixedCryptr.decrypt(encryptedData));
2629
} catch (e) {
27-
// ciphertext is broken
28-
return err(new SystemError("Core", "DecryptionError", "Cipher text is broken"));
30+
try {
31+
return ok(this.cryptr.decrypt(encryptedData));
32+
} catch (e2) {
33+
return err(new SystemError("Core", "DecryptionError", "Cipher text is broken"));
34+
}
2935
}
3036
}
3137
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
import { assert } from "chai";
5+
import "mocha";
6+
import { createSandbox } from "sinon";
7+
import Cryptr from "cryptr";
8+
import { LocalCrypto } from "../../src/core/crypto";
9+
import { SystemError } from "@microsoft/teamsfx-api";
10+
11+
describe("LocalCrypto", () => {
12+
const sandbox = createSandbox();
13+
const testProjectId = "test-project-123";
14+
const testPlaintext = "sensitive-data-to-encrypt";
15+
const prefix = "crypto_";
16+
17+
let localCrypto: LocalCrypto;
18+
let fixedCryptr: Cryptr;
19+
let projectCryptr: Cryptr;
20+
21+
beforeEach(() => {
22+
localCrypto = new LocalCrypto(testProjectId);
23+
fixedCryptr = new Cryptr("teamsfx_global_key");
24+
projectCryptr = new Cryptr(testProjectId + "_teamsfx");
25+
});
26+
27+
afterEach(() => {
28+
sandbox.restore();
29+
});
30+
31+
describe("encrypt", () => {
32+
it("should encrypt plaintext with fixed global key and add prefix", () => {
33+
const result = localCrypto.encrypt(testPlaintext);
34+
35+
assert.isTrue(result.isOk());
36+
if (result.isOk()) {
37+
const encrypted = result.value;
38+
assert.isTrue(encrypted.startsWith(prefix));
39+
40+
const encryptedData = encrypted.substr(prefix.length);
41+
const decrypted = fixedCryptr.decrypt(encryptedData);
42+
assert.equal(decrypted, testPlaintext);
43+
}
44+
});
45+
});
46+
47+
describe("decrypt", () => {
48+
it("should decrypt strings encrypted with fixed global key (new encryption)", () => {
49+
const encrypted = fixedCryptr.encrypt(testPlaintext);
50+
const ciphertext = prefix + encrypted;
51+
52+
const result = localCrypto.decrypt(ciphertext);
53+
54+
assert.isTrue(result.isOk());
55+
if (result.isOk()) {
56+
assert.equal(result.value, testPlaintext);
57+
}
58+
});
59+
60+
it("should decrypt strings encrypted with project-specific key (old encryption fallback)", () => {
61+
const encrypted = projectCryptr.encrypt(testPlaintext);
62+
const ciphertext = prefix + encrypted;
63+
64+
const result = localCrypto.decrypt(ciphertext);
65+
66+
assert.isTrue(result.isOk());
67+
if (result.isOk()) {
68+
assert.equal(result.value, testPlaintext);
69+
}
70+
});
71+
72+
it("should return error when both fixed and project cryptr fail to decrypt", () => {
73+
const invalidCiphertext = prefix + "invalid-cipher-text-that-cannot-be-decrypted";
74+
75+
const result = localCrypto.decrypt(invalidCiphertext);
76+
77+
assert.isTrue(result.isErr());
78+
if (result.isErr()) {
79+
assert.instanceOf(result.error, SystemError);
80+
assert.equal(result.error.source, "Core");
81+
assert.equal(result.error.name, "DecryptionError");
82+
assert.equal(result.error.message, "Cipher text is broken");
83+
}
84+
});
85+
86+
it("should successfully encrypt and decrypt data (round trip)", () => {
87+
const encryptResult = localCrypto.encrypt(testPlaintext);
88+
assert.isTrue(encryptResult.isOk());
89+
90+
if (encryptResult.isOk()) {
91+
const decryptResult = localCrypto.decrypt(encryptResult.value);
92+
assert.isTrue(decryptResult.isOk());
93+
94+
if (decryptResult.isOk()) {
95+
assert.equal(decryptResult.value, testPlaintext);
96+
}
97+
}
98+
});
99+
100+
it("should handle cross-project compatibility for new encryption", () => {
101+
const crypto1 = new LocalCrypto("project1");
102+
const crypto2 = new LocalCrypto("project2");
103+
104+
// New encryption uses fixed global key, so it works across different project IDs
105+
const encryptResult = crypto1.encrypt(testPlaintext);
106+
assert.isTrue(encryptResult.isOk());
107+
108+
if (encryptResult.isOk()) {
109+
const decryptResult = crypto2.decrypt(encryptResult.value);
110+
assert.isTrue(decryptResult.isOk());
111+
112+
if (decryptResult.isOk()) {
113+
assert.equal(decryptResult.value, testPlaintext);
114+
}
115+
}
116+
});
117+
118+
it("should not decrypt legacy data from different project IDs", () => {
119+
const project1Crypto = new LocalCrypto("project1");
120+
const project2Crypto = new LocalCrypto("project2");
121+
const project1Cryptr = new Cryptr("project1_teamsfx");
122+
123+
// Create legacy encrypted data with project1 key
124+
const legacyEncrypted = prefix + project1Cryptr.encrypt(testPlaintext);
125+
126+
// project1 crypto should decrypt it (fallback to project-specific key)
127+
const project1Result = project1Crypto.decrypt(legacyEncrypted);
128+
assert.isTrue(project1Result.isOk());
129+
if (project1Result.isOk()) {
130+
assert.equal(project1Result.value, testPlaintext);
131+
}
132+
133+
// project2 crypto should fail to decrypt it (different project key)
134+
const project2Result = project2Crypto.decrypt(legacyEncrypted);
135+
assert.isTrue(project2Result.isErr());
136+
});
137+
});
138+
});

0 commit comments

Comments
 (0)