Skip to content

Commit e3d3457

Browse files
committed
SDK alignment with python
update
1 parent 4560b7d commit e3d3457

File tree

10 files changed

+746
-309
lines changed

10 files changed

+746
-309
lines changed

README.md

Lines changed: 368 additions & 14 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@coti-io/coti-sdk-typescript",
3-
"version": "0.5.0",
3+
"version": "0.5.5",
44
"main": "dist/index.js",
55
"types": "dist/index.d.ts",
66
"files": [
Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,26 @@
1-
import { BaseWallet, Wallet, Contract, Provider } from "ethers"
2-
import { decryptString, decryptUint, prepareStringIT, prepareUintIT } from "../libs/crypto"
3-
import { getDefaultProvider } from "../provider"
4-
import { onboard } from "./onboard"
1+
import {BaseWallet, Contract, Provider, Wallet} from "ethers"
2+
import {buildInputText} from "../crypto_utils"
3+
import {onboard} from "./onboard"
4+
import {decryptUint, initEtherProvider} from "../ethers_utils";
55

66
export class ConfidentialAccount {
7-
constructor(readonly wallet: BaseWallet, readonly userKey: string) {}
7+
constructor(readonly wallet: BaseWallet, readonly userKey: string) {
8+
}
89

9-
public decryptValue(ciphertextValue: bigint) {
10-
return decryptUint(ciphertextValue, this.userKey)
11-
}
10+
public static async onboard(wallet: BaseWallet, contract?: Contract): Promise<ConfidentialAccount> {
11+
const userKey = await onboard(wallet, contract)
12+
return new ConfidentialAccount(wallet, userKey)
13+
}
1214

13-
public encryptValue(plaintextValue: bigint | number, contractAddress: string, functionSelector: string) {
14-
return prepareUintIT(BigInt(plaintextValue), this, contractAddress, functionSelector)
15-
}
15+
public static createWallet(provider?: Provider): BaseWallet {
16+
return Wallet.createRandom(provider ?? initEtherProvider())
17+
}
1618

17-
public static async onboard(wallet: BaseWallet, contract?: Contract): Promise<ConfidentialAccount> {
18-
const userKey = await onboard(wallet, contract)
19-
return new ConfidentialAccount(wallet, userKey)
20-
}
19+
public decryptValue(ciphertextValue: bigint) {
20+
return decryptUint(ciphertextValue, this.userKey)
21+
}
2122

22-
public static createWallet(provider?: Provider): BaseWallet {
23-
return Wallet.createRandom(provider ?? getDefaultProvider())
24-
}
23+
public encryptValue(plaintextValue: bigint | number, contractAddress: string, functionSelector: string) {
24+
return buildInputText(BigInt(plaintextValue), this, contractAddress, functionSelector)
25+
}
2526
}

src/account/onboard.ts

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
1-
import { BaseWallet, keccak256, Contract, Signer } from "ethers"
2-
import { generateRSAKeyPair, decryptRSA, sign } from "../libs/crypto"
3-
import { ONBOARD_CONTRACT_ADDRESS, ONBOARD_CONTRACT_ABI } from "./onboard-contract"
1+
import {BaseWallet, Contract, keccak256, Signer} from "ethers"
2+
import {decryptRSA, generateRSAKeyPair, sign} from "../crypto_utils"
3+
import {ONBOARD_CONTRACT_ABI, ONBOARD_CONTRACT_ADDRESS} from "./onboard-contract"
44

55
function getDefaultContract(wallet: Signer) {
6-
return new Contract(ONBOARD_CONTRACT_ADDRESS, JSON.stringify(ONBOARD_CONTRACT_ABI), wallet)
6+
return new Contract(ONBOARD_CONTRACT_ADDRESS, JSON.stringify(ONBOARD_CONTRACT_ABI), wallet)
77
}
88

99
export async function onboard(user: BaseWallet, contract = getDefaultContract(user)) {
10-
const { publicKey, privateKey } = generateRSAKeyPair()
10+
const {publicKey, privateKey} = generateRSAKeyPair()
1111

12-
const signedEK = sign(keccak256(publicKey), user.privateKey)
13-
const receipt = await (await contract.OnboardAccount(publicKey, signedEK, { gasLimit: 12000000 })).wait()
14-
if (!receipt || !receipt.logs || !receipt.logs[0]) {
15-
throw new Error("failed to onboard account")
16-
}
17-
const decodedLog = contract.interface.parseLog(receipt.logs[0])
18-
if (!decodedLog) {
19-
throw new Error("failed to onboard account")
20-
}
21-
const encryptedKey = decodedLog.args.userKey
22-
const buf = Buffer.from(encryptedKey.substring(2), "hex")
23-
return decryptRSA(privateKey, buf).toString("hex")
12+
const signedEK = sign(keccak256(publicKey), user.privateKey)
13+
const receipt = await (await contract.OnboardAccount(publicKey, signedEK, {gasLimit: 12000000})).wait()
14+
if (!receipt || !receipt.logs || !receipt.logs[0]) {
15+
throw new Error("failed to onboard account")
16+
}
17+
const decodedLog = contract.interface.parseLog(receipt.logs[0])
18+
if (!decodedLog) {
19+
throw new Error("failed to onboard account")
20+
}
21+
const encryptedKey = decodedLog.args.userKey
22+
const buf = Buffer.from(encryptedKey.substring(2), "hex")
23+
return decryptRSA(privateKey, buf).toString("hex")
2424
}

src/crypto_utils.ts

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import crypto from "crypto"
2+
import {BaseWallet, ethers, getBytes, SigningKey, solidityPackedKeccak256} from "ethers"
3+
import * as fs from "node:fs";
4+
5+
const block_size = 16 // AES block size in bytes
6+
const hexBase = 16
7+
8+
export function encrypt(key: Buffer, plaintext: Buffer): { ciphertext: Buffer; r: Buffer } {
9+
// Ensure plaintext is smaller than 128 bits (16 bytes)
10+
if (plaintext.length > block_size) {
11+
throw new RangeError("Plaintext size must be 128 bits or smaller.")
12+
}
13+
// Ensure key size is 128 bits (16 bytes)
14+
if (key.length != block_size) {
15+
throw new RangeError("Key size must be 128 bits.")
16+
}
17+
// Create a new AES cipher using the provided key
18+
const cipher = crypto.createCipheriv("aes-128-ecb", key, null)
19+
20+
// Generate a random value 'r' of the same length as the block size
21+
const r = crypto.randomBytes(block_size)
22+
23+
// Encrypt the random value 'r' using AES in ECB mode
24+
const encryptedR = cipher.update(r)
25+
26+
// Pad the plaintext with zeros if it's smaller than the block size
27+
const plaintext_padded = Buffer.concat([Buffer.alloc(block_size - plaintext.length), plaintext])
28+
29+
// XOR the encrypted random value 'r' with the plaintext to obtain the ciphertext
30+
const ciphertext = Buffer.alloc(encryptedR.length)
31+
for (let i = 0; i < encryptedR.length; i++) {
32+
ciphertext[i] = encryptedR[i] ^ plaintext_padded[i]
33+
}
34+
35+
return {ciphertext, r}
36+
}
37+
38+
export function decrypt(key: Buffer, r: Buffer, ciphertext: Buffer): Buffer {
39+
if (ciphertext.length !== block_size) {
40+
throw new RangeError("Ciphertext size must be 128 bits.")
41+
}
42+
43+
// Ensure key size is 128 bits (16 bytes)
44+
if (key.length != block_size) {
45+
throw new RangeError("Key size must be 128 bits.")
46+
}
47+
48+
// Ensure random size is 128 bits (16 bytes)
49+
if (r.length != block_size) {
50+
throw new RangeError("Random size must be 128 bits.")
51+
}
52+
53+
// Create a new AES decipher using the provided key
54+
const cipher = crypto.createCipheriv("aes-128-ecb", key, null)
55+
56+
// Encrypt the random value 'r' using AES in ECB mode
57+
const encryptedR = cipher.update(r)
58+
59+
// XOR the encrypted random value 'r' with the ciphertext to obtain the plaintext
60+
const plaintext = Buffer.alloc(encryptedR.length)
61+
for (let i = 0; i < encryptedR.length; i++) {
62+
plaintext[i] = encryptedR[i] ^ ciphertext[i]
63+
}
64+
65+
return plaintext
66+
}
67+
68+
export function generateRSAKeyPair(): crypto.KeyPairSyncResult<Buffer, Buffer> {
69+
// Generate a new RSA key pair
70+
return crypto.generateKeyPairSync("rsa", {
71+
modulusLength: 2048,
72+
publicKeyEncoding: {
73+
type: "spki",
74+
format: "der", // Specify 'der' format for binary data
75+
},
76+
privateKeyEncoding: {
77+
type: "pkcs8",
78+
format: "der", // Specify 'der' format for binary data
79+
},
80+
})
81+
}
82+
83+
export function decryptRSA(privateKey: Buffer, ciphertext: Buffer): Buffer {
84+
// Load the private key in PEM format
85+
let privateKeyPEM = privateKey.toString("base64")
86+
privateKeyPEM = `-----BEGIN PRIVATE KEY-----\n${privateKeyPEM}\n-----END PRIVATE KEY-----`
87+
// Decrypt the ciphertext using RSA-OAEP
88+
return crypto.privateDecrypt(
89+
{
90+
key: privateKeyPEM,
91+
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
92+
oaepHash: "sha256",
93+
},
94+
ciphertext
95+
)
96+
}
97+
98+
99+
export function sign(message: string, privateKey: string) {
100+
const key = new SigningKey(privateKey)
101+
const sig = key.sign(message)
102+
return Buffer.concat([getBytes(sig.r), getBytes(sig.s), getBytes(`0x0${sig.v - 27}`)])
103+
}
104+
105+
export function keccak256(publicKey: Buffer) {
106+
return ethers.keccak256(publicKey);
107+
}
108+
109+
export function signInputText(sender: {
110+
wallet: BaseWallet;
111+
userKey: string
112+
}, contractAddress: string, functionSelector: string, ct: Buffer) {
113+
const message = solidityPackedKeccak256(
114+
["address", "address", "bytes4", "uint256"],
115+
[sender.wallet.address, contractAddress, functionSelector, BigInt("0x" + ct.toString("hex"))]
116+
)
117+
118+
return sign(message, sender.wallet.privateKey);
119+
}
120+
121+
export function buildInputText(
122+
plaintext: bigint,
123+
sender: { wallet: BaseWallet; userKey: string },
124+
contractAddress: string,
125+
functionSelector: string
126+
) {
127+
// Convert the plaintext to bytes
128+
const plaintextBytes = Buffer.alloc(8) // Allocate a buffer of size 8 bytes
129+
plaintextBytes.writeBigUInt64BE(plaintext) // Write the uint64 value to the buffer as little-endian
130+
131+
// Encrypt the plaintext using AES key
132+
const {ciphertext, r} = encrypt(Buffer.from(sender.userKey, "hex"), plaintextBytes)
133+
const ct = Buffer.concat([ciphertext, r])
134+
135+
const signature = signInputText(sender, contractAddress, functionSelector, ct);
136+
137+
// Convert the ciphertext to BigInt
138+
const ctInt = BigInt("0x" + ct.toString("hex"))
139+
140+
return {ctInt, signature}
141+
}
142+
143+
export async function buildStringInputText(
144+
plaintext: string,
145+
sender: { wallet: BaseWallet; userKey: string },
146+
contractAddress: string,
147+
functionSelector: string
148+
) {
149+
let encoder = new TextEncoder()
150+
151+
let encodedStr = encoder.encode(plaintext)
152+
153+
let encryptedStr = new Array<{ ciphertext: bigint, signature: Buffer }>(plaintext.length)
154+
155+
for (let i = 0; i < plaintext.length; i++) {
156+
const {ctInt, signature} = buildInputText(BigInt(encodedStr[i]), sender, contractAddress, functionSelector)
157+
encryptedStr[i] = {ciphertext: ctInt, signature}
158+
}
159+
160+
return encryptedStr
161+
}
162+
163+
export function generateAesKey() {
164+
return crypto.randomBytes(block_size).toString("hex")
165+
}
166+
167+
export function loadAesKey(filePath: string): Buffer {
168+
const hexKey = fs.readFileSync(filePath, 'utf8').trim();
169+
const key = Buffer.from(hexKey, 'hex');
170+
if (key.length !== block_size) {
171+
throw new Error(`Invalid key length: ${key.length} bytes, must be ${block_size} bytes`);
172+
}
173+
return key;
174+
}
175+
176+
export function writeAesKey(filePath: string, key: Buffer): void {
177+
if (key.length !== block_size) {
178+
throw new Error(`Invalid key length: ${key.length} bytes, must be ${block_size} bytes`);
179+
}
180+
const hexKey = key.toString('hex');
181+
fs.writeFileSync(filePath, hexKey);
182+
}
183+
184+

0 commit comments

Comments
 (0)