Skip to content

Commit

Permalink
update interfaces to support new keys/backward compatible
Browse files Browse the repository at this point in the history
  • Loading branch information
samantehrani committed Jan 16, 2025
1 parent 585c57d commit 39520d8
Show file tree
Hide file tree
Showing 15 changed files with 227 additions and 124 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"clean:bundle": "rimraf bundles",
"clean": "npm run clean:dist && npm run clean:package && npm run clean:bundle",
"prepublishOnly": "npm run clean && npm run build",
"test": "mocha test/*.ts && echo \"NOW RUN => 'npm run test:web' <= \" ",
"test": "mocha --file test/shims.ts test/*.ts && echo \"NOW RUN => 'npm run test:web' <= \" ",
"test:web": "npm run bundle:web && webpack --config-name web-tests && opener test/web/web.html",
"prettier:check": "prettier --check \"src/**/*.ts\" \"test/**/*.ts\"",
"prettier:write": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
Expand Down
17 changes: 13 additions & 4 deletions src/common/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import * as ArweaveUtils from "./lib/utils";
import Silo from "./silo";
import Chunks from "./chunks";
import Blocks from "./blocks";
import { PrivateKey, PublicKey, fromJWK } from "./lib/crypto/keys";

export interface Config {
api: ApiConfig;
Expand Down Expand Up @@ -83,7 +84,7 @@ export default class Arweave {

public async createTransaction(
attributes: Partial<CreateTransactionInterface>,
jwk?: JWKInterface | "use_wallet"
keyData?: JWKInterface | "use_wallet" | PrivateKey | PublicKey
): Promise<Transaction> {
const transaction: Partial<CreateTransactionInterface> = {};

Expand All @@ -95,10 +96,18 @@ export default class Arweave {
);
}

if (attributes.owner == undefined) {
if (jwk && jwk !== "use_wallet") {
transaction.owner = jwk.n;
if (attributes.owner == undefined && keyData && keyData !== "use_wallet") {
let pk: PublicKey;
if (keyData instanceof PrivateKey){
pk = await keyData.public()
} else if (keyData instanceof PublicKey) {
pk = keyData;
} else {
pk = await fromJWK(keyData as JsonWebKey)
.then(sk => sk.public());
}
transaction.owner = await pk.identifier()
.then(id => ArweaveUtils.bufferTob64Url(id));
}

if (attributes.last_tx == undefined) {
Expand Down
3 changes: 2 additions & 1 deletion src/common/lib/crypto/crypto-interface.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { JWKInterface } from "../wallet";
import { PrivateKey } from "./keys";

export interface SignatureOptions {
saltLength?: number;
Expand All @@ -8,7 +9,7 @@ export default interface CryptoInterface {
generateJWK(): Promise<JWKInterface>;

sign(
jwk: JWKInterface,
jwk: JWKInterface | PrivateKey,
data: Uint8Array,
options?: SignatureOptions
): Promise<Uint8Array>;
Expand Down
6 changes: 3 additions & 3 deletions src/common/lib/crypto/keys/ec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { KeyType, KeyTypeByte, PublicKey, PrivateKey, getInitializationOptions, getSigningParameters, SerializationParams } from "./interface";
import { KeyType, KeyTypeByte, PublicKey, PrivateKey, getInitializationOptions, getSigningParameters, SerializationParams, SigningParams, VerifyingParams } from "./interface";

export class EllipticCurvePrivateKey extends PrivateKey {
static usages: Array<KeyUsage> = ["sign", "verify"];
Expand Down Expand Up @@ -38,7 +38,7 @@ export class EllipticCurvePrivateKey extends PrivateKey {
}
}

public async sign({payload}: {payload: Uint8Array}): Promise<Uint8Array> {
public async sign({payload}: SigningParams): Promise<Uint8Array> {
return new Uint8Array(await this.driver.sign(
getSigningParameters(this.type),
this.key,
Expand Down Expand Up @@ -88,7 +88,7 @@ export class EllipticCurvePublicKey extends PublicKey {
this.key = key;
}

public async verify({payload, signature}: {payload: Uint8Array, signature: Uint8Array}): Promise<boolean> {
public async verify({payload, signature}: VerifyingParams): Promise<boolean> {
switch(this.type) {
case KeyType.ED_25519:
return this.driver.verify(
Expand Down
9 changes: 5 additions & 4 deletions src/common/lib/crypto/keys/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { RSAPrivateKey, RSAPublicKey } from './rsa';
export { EllipticCurvePrivateKey, EllipticCurvePublicKey } from './ec';
export { SECP256k1PrivateKey, SECP256k1PublicKey } from './secp256k1';
export { KeyType, PrivateKey, PublicKey } from './interface';
export { RSAPrivateKey, RSAPublicKey } from "./rsa";
export { EllipticCurvePrivateKey, EllipticCurvePublicKey } from "./ec";
export { SECP256k1PrivateKey, SECP256k1PublicKey } from "./secp256k1";
export { KeyType, PrivateKey, PublicKey } from "./interface";
export { fromJWK, fromIdentifier } from "./utils";
7 changes: 4 additions & 3 deletions src/common/lib/crypto/keys/interface.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import { Secp256k1 } from "@solar-republic/wasm-secp256k1";

export enum KeyType {
RSA_65537 = "rsa_65537",
EC_SECP256K1 = "ec_secp256k1",
ED_25519 = "ed_25519",
};

export const KeyTypeByte = {
[KeyType.RSA_65537]: null,
[KeyType.EC_SECP256K1]: 2,
[KeyType.ED_25519]: 3
}
export const KeyTypeBytesReverse: Map<number, string> = new Map(Object.entries(KeyTypeByte).map(([key, value]) => ([value, key])));

export type Format = "jwk" | "raw";

Expand All @@ -20,15 +18,18 @@ export interface SerializationParams<T extends Format = Format> {

export interface SigningParams {
payload: Uint8Array;
is_digest?: boolean;
}

export interface VerifyingParams {
payload: Uint8Array;
signature: Uint8Array;
is_digest?: boolean;
}

export interface EncryptionParams {
secret: Uint8Array;

}

export interface DecryptionParams {
Expand Down
53 changes: 42 additions & 11 deletions src/common/lib/crypto/keys/rsa.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { KeyType, PublicKey, PrivateKey, getInitializationOptions, getSigningParameters, SerializationParams } from "./interface";
import { KeyType, PublicKey, PrivateKey, getInitializationOptions, getSigningParameters, SerializationParams, VerifyingParams, SigningParams } from "./interface";
import { b64UrlToBuffer, bufferTob64Url } from "../../utils";

export class RSAPrivateKey extends PrivateKey {
static usages: Array<KeyUsage> = ["sign", "verify"];
static usages: Array<KeyUsage> = ["sign"];
static async new({driver = crypto.subtle, type = KeyType.RSA_65537, modulusLength}: {driver?: SubtleCrypto, type?: KeyType, modulusLength?: number} = {driver: crypto.subtle, type: KeyType.RSA_65537}): Promise<RSAPrivateKey> {
if (modulusLength !== undefined) {
if (modulusLength < 32 * 8 || modulusLength > 512 * 8) {
Expand Down Expand Up @@ -47,9 +48,13 @@ export class RSAPrivateKey extends PrivateKey {

}

public async sign({payload}: {payload: Uint8Array}): Promise<Uint8Array> {
public async sign({payload}: SigningParams, saltLength?: number): Promise<Uint8Array> {
let signingOptions = getSigningParameters(this.type) as RsaPssParams;
if (saltLength !== undefined) {
signingOptions = {...signingOptions, saltLength};
}
return new Uint8Array(await this.driver.sign(
getSigningParameters(this.type),
signingOptions,
this.key,
payload
));
Expand All @@ -59,8 +64,14 @@ export class RSAPrivateKey extends PrivateKey {
if (this.publicKey !== null) {
return this.publicKey;
}
let keyData = await this.driver.exportKey("spki", this.key);
this.publicKey = await RSAPublicKey.deserialize({driver: this.driver, format: "spki", keyData, type: this.type});
let keyData = await this.driver.exportKey("jwk", this.key);
delete keyData.d;
delete keyData.dp;
delete keyData.dq;
delete keyData.q;
delete keyData.qi;
delete keyData.key_ops;
this.publicKey = await RSAPublicKey.deserialize({driver: this.driver, format: "jwk", keyData, type: this.type});
return this.publicKey;
}

Expand Down Expand Up @@ -89,11 +100,19 @@ export class RSAPublicKey extends PublicKey {
}

static async deserialize({driver = crypto.subtle, format, keyData, type}: {driver?: SubtleCrypto, format: "jwk" | "raw" | "pkcs8" | "spki", keyData: JsonWebKey | ArrayBuffer, type: KeyType}): Promise<RSAPublicKey> {
if (format === "raw") {
keyData = {
kty: "RSA",
e: "AQAB",
n: bufferTob64Url(keyData as Uint8Array),
};
format = "jwk";
}
const key = await driver.importKey(format as any, keyData as any, getInitializationOptions(type), true, RSAPublicKey.usages);
return new RSAPublicKey({driver, type, key});
}

public async verify({payload, signature}: {payload: Uint8Array, signature: Uint8Array}): Promise<boolean> {
public async verify({payload, signature}: VerifyingParams): Promise<boolean> {
switch(this.type) {
case KeyType.RSA_65537: {
let result = false;
Expand All @@ -114,12 +133,24 @@ export class RSAPublicKey extends PublicKey {
}
}

public async identifier(): Promise<Uint8Array> {
return await this.serialize({format: "raw"}) as Uint8Array;
public async serialize({format}: SerializationParams<"jwk">): Promise<JsonWebKey>;
public async serialize({format}: SerializationParams<"raw">): Promise<Uint8Array>;
public async serialize({format}: SerializationParams): Promise<JsonWebKey | Uint8Array> {
const jwk = await this.driver.exportKey("jwk", this.key);
switch(format) {
case "jwk":
return jwk;
case "raw":
return b64UrlToBuffer(jwk.n!);
default:
throw new Error(`Unsupported format ${format}`);
}
}
public async serialize(params: SerializationParams): Promise<JsonWebKey | Uint8Array> {
throw new Error("not implemented");

public async identifier(): Promise<Uint8Array> {
return this.serialize({format: "raw"});
}

}

const maxSaltSize = (key: CryptoKey): number => {
Expand Down
58 changes: 42 additions & 16 deletions src/common/lib/crypto/keys/secp256k1.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Secp256k1, initWasmSecp256k1 } from "@solar-republic/wasm-secp256k1";
import { KeyType, KeyTypeByte, PrivateKey, PublicKey, SerializationParams } from "./interface";
import { KeyType, KeyTypeByte, PrivateKey, PublicKey, SerializationParams, SigningParams, VerifyingParams } from "./interface";
import { bufferTob64Url, b64UrlToBuffer } from "../../utils";

// TODO: build wasm module and internalize the dependency
Expand Down Expand Up @@ -69,14 +69,28 @@ export class SECP256k1PrivateKey extends PrivateKey {
}
}

public async sign({payload}: {payload: Uint8Array}): Promise<Uint8Array> {
const[signature, _recovery] = this.driver.sign(this.key, payload);
public async sign({payload, is_digest = false}: SigningParams): Promise<Uint8Array> {
let digest = payload;
if (is_digest == false) {
digest = new Uint8Array(await crypto.subtle.digest("SHA-256", payload));
}
const[signature, _recovery] = this.driver.sign(this.key, digest);
return signature;
}
}

export class SECP256k1PublicKey extends PublicKey {
static usages: Array<KeyUsage> = ["verify"];
static async fromIdentifier({identifier}: {identifier: Uint8Array}): Promise<SECP256k1PublicKey> {
if (identifier[0] !== KeyTypeByte[KeyType.EC_SECP256K1]) {
throw new Error("Invalid prefix");
}
if (identifier.byteLength !== 35) {
throw new Error("Invalid identifier length");
}
const rawCompressed = identifier.slice(1, 34);
return SECP256k1PublicKey.deserialize({format: "raw", keyData: rawCompressed});
}
static async deserialize({driver = null, format, keyData}: {driver?: Secp256k1 | null, format: "jwk" | "raw", keyData: JsonWebKey | Uint8Array}): Promise<SECP256k1PublicKey> {
if (driver === null) {
driver = (await ENGINE);
Expand All @@ -88,14 +102,14 @@ export class SECP256k1PublicKey extends PublicKey {
}
const x = b64UrlToBuffer(k.x!);
const y = b64UrlToBuffer(k.y!);
if (x.length !== 32 || y.length !== 32) {
throw new Error(`Invalid secp256k1 PublicKey coordinate size: X: ${x.length}, Y: ${y.length}`);
if (x.byteLength !== 32 || y.byteLength !== 32) {
throw new Error(`Invalid secp256k1 PublicKey coordinate size: X: ${x.byteLength}, Y: ${y.byteLength}`);
}
return new SECP256k1PublicKey({driver, key: Uint8Array.from([0x04, ...x, ...y])});
} else {
const key = keyData as Uint8Array;
if (key.length !== 65) {
throw new Error(`Invalid secp256k1 PublicKey size ${key.length}`);
if (!([33, 65].includes(key.byteLength))) {
throw new Error(`Invalid secp256k1 PublicKey size ${key.byteLength}`);
}
return new SECP256k1PublicKey({driver, key});
}
Expand All @@ -104,16 +118,28 @@ export class SECP256k1PublicKey extends PublicKey {
private readonly driver: Secp256k1;
private readonly key: Uint8Array;
constructor({driver, key}: {driver: Secp256k1, key: Uint8Array}) {
if (key.length !== 65 || key[0] !== 0x04) {
throw new Error('Only uncompressed format accepted for initialization!');
if (key.byteLength === 65) {
if (key[0] !== 0x04) {
throw new Error('Unaccepted uncompressed format prefix!');
}
} else if(key.byteLength === 33) {
if (![0x02, 0x03].includes(key[0])) {
throw new Error('Unaccepted compressed format prefix!');
}
} else {
throw new Error(`Incorrect public key size: ${key.byteLength}`);
}
super({type: KeyType.EC_SECP256K1});
this.driver = driver;
this.key = key;
}

public async verify({payload, signature}: {payload: Uint8Array, signature: Uint8Array}): Promise<boolean> {
return this.driver.verify(signature, payload, this.key);
public async verify({payload, signature, is_digest = false}: VerifyingParams): Promise<boolean> {
let digest = payload;
if (is_digest === false) {
digest = new Uint8Array(await crypto.subtle.digest("SHA-256", payload));
}
return this.driver.verify(signature, digest, this.key);
}

public async serialize({format}: SerializationParams<"jwk">): Promise<JsonWebKey>;
Expand All @@ -130,14 +156,14 @@ export class SECP256k1PublicKey extends PublicKey {
case "raw":
const x = this.key.slice(1, 33);
const y = this.key.slice(33);
const raw_compressed = new Uint8Array(33);
const rawCompressed = new Uint8Array(33);
if ((y[31] & 1) === 1) {
raw_compressed[0] = 3;
rawCompressed[0] = 3;
} else {
raw_compressed[0] = 2;
rawCompressed[0] = 2;
}
raw_compressed.set(x, 1);
return raw_compressed;
rawCompressed.set(x, 1);
return rawCompressed;
default:
throw new Error(`Unsupported format ${format}`);
}
Expand Down
32 changes: 32 additions & 0 deletions src/common/lib/crypto/keys/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { EllipticCurvePrivateKey } from "./ec";
import { KeyType, PrivateKey, PublicKey, KeyTypeBytesReverse } from "./interface";
import { RSAPrivateKey, RSAPublicKey } from "./rsa";
import { SECP256k1PrivateKey, SECP256k1PublicKey } from "./secp256k1"

export const fromJWK = async (keyData: JsonWebKey): Promise<PrivateKey> => {
const format = "jwk";
switch(keyData.kty) {
case "EC":
return SECP256k1PrivateKey.deserialize({format, keyData});
case "OKP":
return EllipticCurvePrivateKey.deserialize({format, keyData, type: KeyType.ED_25519});
case "RSA":
return RSAPrivateKey.deserialize({format, keyData, type: KeyType.RSA_65537});
default:
throw new Error(`Unsupported kty ${keyData.kty}`);
}
}

export const fromIdentifier = async ({identifier}: {identifier: Uint8Array}): Promise<PublicKey> => {
if (identifier.byteLength % 2 == 0) {
return RSAPublicKey.deserialize({format: "raw", keyData: identifier, type: KeyType.RSA_65537});
}
const keyTypeByte = identifier[0];
switch (KeyTypeBytesReverse.get(keyTypeByte)) {
case "ec_secp256k1":
return SECP256k1PublicKey.fromIdentifier({identifier})
default:
throw new Error(`Unknown KeyType byte prefix ${keyTypeByte}`);
}

};
Loading

0 comments on commit 39520d8

Please sign in to comment.