diff --git a/README.md b/README.md
index e98bc56..2e0b37f 100644
--- a/README.md
+++ b/README.md
@@ -14,9 +14,9 @@ KeyPair.fromPem(string); // => KeyPair
Managing identities.
```ts
-identity = Identity.fromPublicKey(key); // => Identity
-identity = Identity.fromString(string); // => Identity
-anonymous = new Identity(); // => Anonymous Identity
+identity = Address.fromPublicKey(key); // => Address
+identity = Address.fromString(string); // => Address
+anonymous = new Address(); // => Anonymous Address
identity.toString(keys); // => "mw7aekyjtsx2hmeadrua5cpitgy7pykjkok3gyth3ggsio4zwa"
identity.toHex(keys); // => "01e736fc9624ff8ca7956189b6c1b66f55f533ed362ca48c884cd20065";
diff --git a/package-lock.json b/package-lock.json
index ba3a4a2..55c0a88 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -22,6 +22,7 @@
"crc": "^3.8.0",
"jest": "^27.4.5",
"js-sha3": "^0.8.0",
+ "js-sha512": "^0.8.0",
"node-forge": "^0.10.0",
"ts-jest": "^27.1.1",
"typescript": "^4.4.4",
@@ -3423,6 +3424,11 @@
"resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz",
"integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q=="
},
+ "node_modules/js-sha512": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/js-sha512/-/js-sha512-0.8.0.tgz",
+ "integrity": "sha512-PWsmefG6Jkodqt+ePTvBZCSMFgN7Clckjd0O7su3I0+BW2QWUTJNzjktHsztGLhncP2h8mcF9V9Y2Ha59pAViQ=="
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -7451,6 +7457,11 @@
"resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz",
"integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q=="
},
+ "js-sha512": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/js-sha512/-/js-sha512-0.8.0.tgz",
+ "integrity": "sha512-PWsmefG6Jkodqt+ePTvBZCSMFgN7Clckjd0O7su3I0+BW2QWUTJNzjktHsztGLhncP2h8mcF9V9Y2Ha59pAViQ=="
+ },
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
diff --git a/package.json b/package.json
index 02051b1..0413e5a 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
"crc": "^3.8.0",
"jest": "^27.4.5",
"js-sha3": "^0.8.0",
+ "js-sha512": "^0.8.0",
"node-forge": "^0.10.0",
"ts-jest": "^27.1.1",
"typescript": "^4.4.4",
@@ -25,6 +26,7 @@
"scripts": {
"test": "jest",
"build": "tsc",
+ "watch": "npm run clean && npm run build -- -w",
"clean": "rm -rf ./dist",
"postinstall": "tsc"
},
@@ -46,6 +48,7 @@
"jest": {
"preset": "ts-jest",
"testEnvironment": "node",
+ "maxWorkers": 1,
"testMatch": [
"**/?(*.)+(spec|test).[jt]s?(x)"
],
diff --git a/src/const.ts b/src/const.ts
new file mode 100644
index 0000000..54e4db2
--- /dev/null
+++ b/src/const.ts
@@ -0,0 +1,2 @@
+export const ONE_SECOND = 1000
+export const ONE_MINUTE = 60000
diff --git a/src/identity/address.ts b/src/identity/address.ts
new file mode 100644
index 0000000..3b74a8c
--- /dev/null
+++ b/src/identity/address.ts
@@ -0,0 +1,81 @@
+import base32Decode from "base32-decode"
+import base32Encode from "base32-encode"
+import crc from "crc"
+
+export const ANON_IDENTITY = "maa"
+export class Address {
+ bytes: Uint8Array
+
+ constructor(bytes?: Buffer) {
+ this.bytes = new Uint8Array(bytes ? bytes : [0x00])
+ }
+
+ static anonymous(): Address {
+ return new Address()
+ }
+
+ static fromHex(hex: string): Address {
+ return new Address(Buffer.from(hex, "hex"))
+ }
+
+ static fromString(string: string): Address {
+ if (string === ANON_IDENTITY) {
+ return new Address()
+ }
+ const base32Address = string.slice(1, -2).toUpperCase()
+ const base32Checksum = string.slice(-2).toUpperCase()
+ const identity = base32Decode(base32Address, "RFC4648")
+ const checksum = base32Decode(base32Checksum, "RFC4648")
+
+ const check = Buffer.allocUnsafe(3)
+ check.writeUInt16BE(crc.crc16(Buffer.from(identity)), 0)
+
+ if (Buffer.compare(Buffer.from(checksum), check.slice(0, 1)) !== 0) {
+ throw new Error("Invalid Checksum")
+ }
+
+ return new Address(Buffer.from(identity))
+ }
+
+ isAnonymous(): boolean {
+ return Buffer.compare(this.toBuffer(), Buffer.from([0x00])) === 0
+ }
+
+ toBuffer(): Buffer {
+ return Buffer.from(this.bytes)
+ }
+
+ toString(): string {
+ if (this.isAnonymous()) {
+ return ANON_IDENTITY
+ }
+ const identity = this.toBuffer()
+ const checksum = Buffer.allocUnsafe(3)
+ checksum.writeUInt16BE(crc.crc16(identity), 0)
+
+ const leader = "m"
+ const base32Address = base32Encode(identity, "RFC4648", {
+ padding: false,
+ })
+ const base32Checksum = base32Encode(checksum, "RFC4648").slice(0, 2)
+ return (leader + base32Address + base32Checksum).toLowerCase()
+ }
+
+ toHex(): string {
+ if (this.isAnonymous()) {
+ return "00"
+ }
+ return Buffer.from(this.bytes).toString("hex")
+ }
+
+ withSubresource(id: number): Address {
+ let bytes = Buffer.from(this.bytes.slice(0, 29))
+ bytes[0] = 0x80 + ((id & 0x7f000000) >> 24)
+ const subresourceBytes = Buffer.from([
+ (id & 0x00ff0000) >> 16,
+ (id & 0x0000ff00) >> 8,
+ id & 0x000000ff,
+ ])
+ return new Address(Buffer.concat([bytes, subresourceBytes]))
+ }
+}
diff --git a/src/identity/anonymous/anonymous-identity.ts b/src/identity/anonymous/anonymous-identity.ts
new file mode 100644
index 0000000..d49208d
--- /dev/null
+++ b/src/identity/anonymous/anonymous-identity.ts
@@ -0,0 +1,20 @@
+import { Identity } from "../types"
+import { EMPTY } from "../../message/cose"
+import { Address } from "../address"
+
+export class AnonymousIdentity extends Identity {
+ async sign() {
+ return EMPTY
+ }
+ async verify() {
+ return false
+ }
+
+ async getAddress(): Promise
{
+ return Address.anonymous()
+ }
+
+ toJSON(): { dataType: string } {
+ return { dataType: this.constructor.name }
+ }
+}
diff --git a/src/identity/anonymous/index.ts b/src/identity/anonymous/index.ts
new file mode 100644
index 0000000..f9451d9
--- /dev/null
+++ b/src/identity/anonymous/index.ts
@@ -0,0 +1 @@
+export { AnonymousIdentity } from "./anonymous-identity"
diff --git a/src/identity/ed25519/__tests__/ed25519-identity.test.ts b/src/identity/ed25519/__tests__/ed25519-identity.test.ts
new file mode 100644
index 0000000..da6f2e0
--- /dev/null
+++ b/src/identity/ed25519/__tests__/ed25519-identity.test.ts
@@ -0,0 +1,47 @@
+import { Ed25519KeyPairIdentity } from "../ed25519-key-pair-identity"
+
+describe("keys", () => {
+ test("getSeedWords", () => {
+ const seedWords = Ed25519KeyPairIdentity.getMnemonic()
+
+ expect(seedWords.split(" ")).toHaveLength(12)
+ })
+
+ test("fromSeedWords", async function () {
+ const seedWords = Ed25519KeyPairIdentity.getMnemonic()
+ const badWords = "abandon abandon abandon"
+
+ const alice = Ed25519KeyPairIdentity.fromMnemonic(seedWords)
+ const bob = Ed25519KeyPairIdentity.fromMnemonic(seedWords)
+
+ const aliceAddress = (await alice.getAddress()).toString()
+ const bobAddress = (await bob.getAddress()).toString()
+
+ expect(aliceAddress).toStrictEqual(bobAddress)
+ expect(() => {
+ Ed25519KeyPairIdentity.fromMnemonic(badWords)
+ }).toThrow()
+ })
+
+ test("fromPem", async function () {
+ const pem = `
+ -----BEGIN PRIVATE KEY-----
+ MC4CAQAwBQYDK2VwBCIEICT3i6WfLx4t3UF6R8aEfczyATc/jvqvOrNga2MJfA2R
+ -----END PRIVATE KEY-----`
+ const badPem = `
+ -----BEGIN PRIVATE CAT-----
+ MEOW
+ -----END PRIVATE CAT-----`
+
+ const alice = Ed25519KeyPairIdentity.fromPem(pem)
+ const bob = Ed25519KeyPairIdentity.fromPem(pem)
+
+ const aliceAddress = (await alice.getAddress()).toString()
+ const bobAddress = (await bob.getAddress()).toString()
+
+ expect(aliceAddress).toStrictEqual(bobAddress)
+ expect(() => {
+ Ed25519KeyPairIdentity.fromPem(badPem)
+ }).toThrow()
+ })
+})
diff --git a/src/identity/ed25519/ed25519-key-pair-identity.ts b/src/identity/ed25519/ed25519-key-pair-identity.ts
new file mode 100644
index 0000000..b159ee9
--- /dev/null
+++ b/src/identity/ed25519/ed25519-key-pair-identity.ts
@@ -0,0 +1,80 @@
+import forge, { pki } from "node-forge"
+import * as bip39 from "bip39"
+import { CoseKey } from "../../message/cose"
+const ed25519 = pki.ed25519
+import { PublicKeyIdentity } from "../types"
+import { Address } from "../address"
+
+export class Ed25519KeyPairIdentity extends PublicKeyIdentity {
+ publicKey: ArrayBuffer
+ protected privateKey: ArrayBuffer
+
+ protected constructor(publicKey: ArrayBuffer, privateKey: ArrayBuffer) {
+ super()
+ this.publicKey = publicKey
+ this.privateKey = privateKey
+ }
+
+ static getMnemonic(): string {
+ return bip39.generateMnemonic()
+ }
+
+ async getAddress(): Promise {
+ const coseKey = this.getCoseKey()
+ return coseKey.toAddress()
+ }
+
+ static fromMnemonic(mnemonic: string): Ed25519KeyPairIdentity {
+ const sanitized = mnemonic.trim().split(/\s+/g).join(" ")
+ if (!bip39.validateMnemonic(sanitized)) {
+ throw new Error("Invalid Mnemonic")
+ }
+ const seed = bip39.mnemonicToSeedSync(sanitized).slice(0, 32)
+ const keys = ed25519.generateKeyPair({ seed })
+ return new Ed25519KeyPairIdentity(keys.publicKey, keys.privateKey)
+ }
+
+ static fromPem(pem: string): Ed25519KeyPairIdentity {
+ try {
+ const der = forge.pem.decode(pem)[0].body
+ const asn1 = forge.asn1.fromDer(der.toString())
+ const { privateKeyBytes } = ed25519.privateKeyFromAsn1(asn1)
+ const keys = ed25519.generateKeyPair({ seed: privateKeyBytes })
+ return new Ed25519KeyPairIdentity(keys.publicKey, keys.privateKey)
+ } catch (e) {
+ throw new Error("Invalid PEM")
+ }
+ }
+
+ async sign(data: ArrayBuffer): Promise {
+ return ed25519.sign({
+ message: data as Uint8Array,
+ privateKey: this.privateKey as Uint8Array,
+ })
+ }
+ async verify(m: ArrayBuffer): Promise {
+ throw new Error("Method not implemented.")
+ }
+
+ getCoseKey(): CoseKey {
+ const c = new Map()
+ c.set(1, 1) // kty: OKP
+ c.set(3, -8) // alg: EdDSA
+ c.set(-1, 6) // crv: Ed25519
+ c.set(4, [2]) // key_ops: [verify]
+ c.set(-2, this.publicKey) // x: publicKey
+ return new CoseKey(c)
+ }
+
+ toJSON(): {
+ dataType: string
+ publicKey: ArrayBuffer
+ privateKey: ArrayBuffer
+ } {
+ return {
+ dataType: this.constructor.name,
+ publicKey: this.publicKey,
+ privateKey: this.privateKey,
+ }
+ }
+}
diff --git a/src/identity/ed25519/index.ts b/src/identity/ed25519/index.ts
new file mode 100644
index 0000000..9c6ff32
--- /dev/null
+++ b/src/identity/ed25519/index.ts
@@ -0,0 +1 @@
+export * from "./ed25519-key-pair-identity"
diff --git a/src/identity/identity.test.ts b/src/identity/identity.test.ts
index 6e8ed05..2196b24 100644
--- a/src/identity/identity.test.ts
+++ b/src/identity/identity.test.ts
@@ -1,4 +1,4 @@
-import { Identity } from "../identity";
+import { Address } from "../identity"
describe("identity", () => {
function id(seed: number) {
@@ -10,67 +10,67 @@ describe("identity", () => {
0, 0, 0, 0, 0, 0, 0, 0,
seed >> 24, seed >> 16, seed >> 8, seed & 0xff,
]);
- return new Identity(Buffer.from(bytes));
+ return new Address(Buffer.from(bytes))
}
test("can read anonymous", () => {
- const anon = new Identity();
- const anonStr = anon.toString();
+ const anon = new Address()
+ const anonStr = anon.toString()
- expect(anon.isAnonymous()).toBe(true);
- expect(anon).toStrictEqual(Identity.fromString(anonStr));
- });
+ expect(anon.isAnonymous()).toBe(true)
+ expect(anon).toStrictEqual(Address.fromString(anonStr))
+ })
test("byte array conversion", () => {
- const anon = new Identity();
- const alice = id(1);
- const bob = id(2);
+ const anon = new Address()
+ const alice = id(1)
+ const bob = id(2)
- expect(anon.toString()).not.toStrictEqual(alice.toString());
- expect(alice.toString()).not.toStrictEqual(bob.toString());
+ expect(anon.toString()).not.toStrictEqual(alice.toString())
+ expect(alice.toString()).not.toStrictEqual(bob.toString())
- expect(anon.toBuffer()).not.toStrictEqual(alice.toBuffer());
- expect(alice.toBuffer()).not.toStrictEqual(bob.toBuffer());
+ expect(anon.toBuffer()).not.toStrictEqual(alice.toBuffer())
+ expect(alice.toBuffer()).not.toStrictEqual(bob.toBuffer())
- expect(Identity.fromString(anon.toString())).toStrictEqual(anon);
- expect(Identity.fromString(alice.toString())).toStrictEqual(alice);
- expect(Identity.fromString(bob.toString())).toStrictEqual(bob);
- });
+ expect(Address.fromString(anon.toString())).toStrictEqual(anon)
+ expect(Address.fromString(alice.toString())).toStrictEqual(alice)
+ expect(Address.fromString(bob.toString())).toStrictEqual(bob)
+ })
test("textual format 1", () => {
- const alice = Identity.fromString(
- "mahek5lid7ek7ckhq7j77nfwgk3vkspnyppm2u467ne5mwiqys"
- );
- const bob = Identity.fromHex(
- "01c8aead03f915f128f0fa7ff696c656eaa93db87bd9aa73df693acb22"
- );
+ const alice = Address.fromString(
+ "mahek5lid7ek7ckhq7j77nfwgk3vkspnyppm2u467ne5mwiqys",
+ )
+ const bob = Address.fromHex(
+ "01c8aead03f915f128f0fa7ff696c656eaa93db87bd9aa73df693acb22",
+ )
- expect(alice).toStrictEqual(bob);
- });
+ expect(alice).toStrictEqual(bob)
+ })
test("textual format 2", () => {
- const alice = Identity.fromString(
- "mqbfbahksdwaqeenayy2gxke32hgb7aq4ao4wt745lsfs6wiaaaaqnz"
- );
- const bob = Identity.fromHex(
- "804a101d521d810211a0c6346ba89bd1cc1f821c03b969ff9d5c8b2f59000001"
- );
+ const alice = Address.fromString(
+ "mqbfbahksdwaqeenayy2gxke32hgb7aq4ao4wt745lsfs6wiaaaaqnz",
+ )
+ const bob = Address.fromHex(
+ "804a101d521d810211a0c6346ba89bd1cc1f821c03b969ff9d5c8b2f59000001",
+ )
- expect(alice).toStrictEqual(bob);
- });
+ expect(alice).toStrictEqual(bob)
+ })
test("subresource 1", () => {
- const alice = Identity.fromString(
- "mahek5lid7ek7ckhq7j77nfwgk3vkspnyppm2u467ne5mwiqys"
- ).withSubresource(1);
- const bob = Identity.fromHex(
- "80c8aead03f915f128f0fa7ff696c656eaa93db87bd9aa73df693acb22000001"
- );
- const charlie = Identity.fromHex(
- "80c8aead03f915f128f0fa7ff696c656eaa93db87bd9aa73df693acb22000002"
- );
+ const alice = Address.fromString(
+ "mahek5lid7ek7ckhq7j77nfwgk3vkspnyppm2u467ne5mwiqys",
+ ).withSubresource(1)
+ const bob = Address.fromHex(
+ "80c8aead03f915f128f0fa7ff696c656eaa93db87bd9aa73df693acb22000001",
+ )
+ const charlie = Address.fromHex(
+ "80c8aead03f915f128f0fa7ff696c656eaa93db87bd9aa73df693acb22000002",
+ )
- expect(alice).toStrictEqual(bob);
- expect(bob.withSubresource(2)).toStrictEqual(charlie);
- });
-});
+ expect(alice).toStrictEqual(bob)
+ expect(bob.withSubresource(2)).toStrictEqual(charlie)
+ })
+})
diff --git a/src/identity/index.ts b/src/identity/index.ts
index 787632b..ffeb473 100644
--- a/src/identity/index.ts
+++ b/src/identity/index.ts
@@ -1,89 +1,5 @@
-import base32Decode from "base32-decode";
-import base32Encode from "base32-encode";
-import crc from "crc";
-
-import { Key } from "../keys";
-import { CoseKey } from "../message/cose";
-
-export const ANON_IDENTITY = "maa"
-export class Identity {
- bytes: Uint8Array;
-
- constructor(bytes?: Buffer) {
- this.bytes = new Uint8Array(bytes ? bytes : [0x00]);
- }
-
- static anonymous(): Identity {
- return new Identity();
- }
-
- static fromHex(hex: string): Identity {
- return new Identity(Buffer.from(hex, "hex"));
- }
-
- static fromPublicKey(key: Key): Identity {
- const coseKey = new CoseKey(key);
- return coseKey.toIdentity();
- }
-
- static fromString(string: string): Identity {
- if (string === ANON_IDENTITY) {
- return new Identity()
- }
- const base32Identity = string.slice(1, -2).toUpperCase();
- const base32Checksum = string.slice(-2).toUpperCase();
- const identity = base32Decode(base32Identity, "RFC4648");
- const checksum = base32Decode(base32Checksum, "RFC4648");
-
- const check = Buffer.allocUnsafe(3);
- check.writeUInt16BE(crc.crc16(Buffer.from(identity)), 0);
-
- if (Buffer.compare(Buffer.from(checksum), check.slice(0, 1)) !== 0) {
- throw new Error("Invalid Checksum");
- }
-
- return new Identity(Buffer.from(identity));
- }
-
- isAnonymous(): boolean {
- return Buffer.compare(this.toBuffer(), Buffer.from([0x00])) === 0;
- }
-
- toBuffer(): Buffer {
- return Buffer.from(this.bytes);
- }
-
- toString(): string {
- if (this.isAnonymous()) {
- return ANON_IDENTITY
- }
- const identity = this.toBuffer();
- const checksum = Buffer.allocUnsafe(3);
- checksum.writeUInt16BE(crc.crc16(identity), 0);
-
- const leader = "m";
- const base32Identity = base32Encode(identity, "RFC4648", {
- padding: false,
- });
- const base32Checksum = base32Encode(checksum, "RFC4648").slice(0, 2);
- return (leader + base32Identity + base32Checksum).toLowerCase();
- }
-
- toHex(): string {
- if (this.isAnonymous()) {
- return "00";
- }
- return Buffer.from(this.bytes).toString("hex");
- }
-
- withSubresource(id: number): Identity {
- let bytes = Buffer.from(this.bytes.slice(0, 29));
- bytes[0] = 0x80 + ((id & 0x7f000000) >> 24);
- const subresourceBytes = Buffer.from([
- (id & 0x00ff0000) >> 16,
- (id & 0x0000ff00) >> 8,
- id & 0x000000ff,
- ]);
- return new Identity(Buffer.concat([bytes, subresourceBytes]));
- }
-}
+export * from "./address"
+export * from "./webauthn"
+export * from "./anonymous"
+export * from "./ed25519"
+export * from "./types"
diff --git a/src/identity/types.ts b/src/identity/types.ts
new file mode 100644
index 0000000..b52b953
--- /dev/null
+++ b/src/identity/types.ts
@@ -0,0 +1,34 @@
+import { CoseKey } from "../message/cose"
+import { Address } from "./address"
+
+export interface Signer {
+ sign(
+ data: ArrayBuffer,
+ unprotectedHeader?: Map,
+ ): Promise
+}
+
+export interface Verifier {
+ verify(data: ArrayBuffer): Promise
+}
+
+export abstract class Identity implements Signer, Verifier {
+ abstract getAddress(): Promise
+ abstract toJSON(): unknown
+ abstract sign(
+ data: ArrayBuffer,
+ unprotectedHeader: Map,
+ ): Promise
+ abstract verify(data: ArrayBuffer): Promise
+ async getUnprotectedHeader(
+ cborMessageContent: ArrayBuffer,
+ cborProtectedHeader: ArrayBuffer,
+ ): Promise