diff --git a/package-lock.json b/package-lock.json index 084122b..73333e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,15 +7,17 @@ "": { "name": "didcomm-demo", "version": "1.0.0", - "license": "ISC", + "license": "MIT", "dependencies": { "@noble/curves": "^1.2.0", + "bs58": "^5.0.0", "didcomm": "^0.4.1", "lit-code": "^0.1.12", "mithril": "^2.2.2", "multibase": "^4.0.6", "multicodec": "^3.2.1", - "uuid": "^9.0.0" + "uuid": "^9.0.0", + "varint": "^6.0.0" }, "devDependencies": { "@fortawesome/fontawesome-free": "^6.4.2", @@ -25,6 +27,7 @@ "@types/mithril": "^2.0.14", "@types/prismjs": "^1.26.0", "@types/uuid": "^9.0.3", + "@types/varint": "^6.0.3", "bulma": "^0.9.4", "css-loader": "^6.8.1", "html-loader": "^4.2.0", @@ -573,6 +576,16 @@ "integrity": "sha512-taHQQH/3ZyI3zP8M/puluDEIEvtQHVYcC6y3N8ijFtAd28+Ey/G4sg1u2gB01S8MwybLOKAp9/yCMu/uR5l3Ug==", "dev": true }, + "node_modules/@types/varint": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/varint/-/varint-6.0.3.tgz", + "integrity": "sha512-DHukoGWdJ2aYkveZJTB2rN2lp6m7APzVsoJQ7j/qy1fQxyamJTPD5xQzCMoJ2Qtgn0mE3wWeNOpbTyBFvF+dyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/ws": { "version": "8.5.5", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz", @@ -939,6 +952,12 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/base-x": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz", + "integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==", + "license": "MIT" + }, "node_modules/batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -1094,6 +1113,15 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bs58": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", + "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==", + "license": "MIT", + "dependencies": { + "base-x": "^4.0.0" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", diff --git a/package.json b/package.json index 5f17b16..70b2184 100644 --- a/package.json +++ b/package.json @@ -29,12 +29,14 @@ }, "dependencies": { "@noble/curves": "^1.2.0", + "bs58": "^5.0.0", "didcomm": "^0.4.1", "lit-code": "^0.1.12", "mithril": "^2.2.2", "multibase": "^4.0.6", "multicodec": "^3.2.1", - "uuid": "^9.0.0" + "uuid": "^9.0.0", + "varint": "^6.0.0" }, "devDependencies": { "@fortawesome/fontawesome-free": "^6.4.2", @@ -44,6 +46,7 @@ "@types/mithril": "^2.0.14", "@types/prismjs": "^1.26.0", "@types/uuid": "^9.0.3", + "@types/varint": "^6.0.3", "bulma": "^0.9.4", "css-loader": "^6.8.1", "html-loader": "^4.2.0", diff --git a/src/lib/agent.ts b/src/lib/agent.ts index 53bd3dd..9891149 100644 --- a/src/lib/agent.ts +++ b/src/lib/agent.ts @@ -62,6 +62,23 @@ export class Agent { case "didGenerated": this.onDidGenerated(e.data.payload) break + case "didRotated": + let contacts = ContactService.getContacts() + const {oldDid, newDid, jwt} = e.data.payload + if(this.profile.did == oldDid) + this.profile.did = newDid + + for (let contact of contacts) { + const message = { + type: "https://didcomm.org/empty/1.0/empty", + body: {}, + from_prior: jwt, + } + logger.log("Sending new did to contact:", contact?.label || contact.did) + console.log("Sending new did to contact:", contact, message) + this.sendMessage(contact, message as IMessage) + } + break case "messageReceived": this.onMessageReceived(e.data.payload) break @@ -143,6 +160,10 @@ export class Agent { } private onMessageReceived(message: IMessage) { + if (message?.prior) { + const oldContact = ContactService.getContact(message.prior.iss) + oldContact && ContactService.rotateContact(oldContact, message.prior.sub) + } const from = message.from == this.profile.did ? (this.profile as Contact) @@ -159,7 +180,7 @@ export class Agent { } ContactService.addMessage(message.from, { sender: fromName, - receiver: to.label || to.did, + receiver: to?.label || to.did, timestamp: new Date(), content: message.body.content, type: message.type, @@ -202,6 +223,13 @@ export class Agent { ContactService.addMessage(contact.did, internalMessage) } + public async rotateDid() { + this.worker.postMessage({ + type: "rotateDid", + payload: {}, + }) + } + public async refreshMessages() { this.postMessage({ type: "pickupStatus", diff --git a/src/lib/contacts.ts b/src/lib/contacts.ts index 7e6e66d..2132276 100644 --- a/src/lib/contacts.ts +++ b/src/lib/contacts.ts @@ -154,6 +154,15 @@ export class EphemeralContactService extends ContactService { this.contacts[contact.did] = contact } + rotateContact(contact: Contact, newDid: string): void { + const old_did = contact.did + delete this.contacts[contact.did] + contact.did = newDid + this.contacts[contact.did] = contact + this.messages[newDid] = this.messages[old_did] + delete this.messages[old_did] + } + saveMessageHistory(did: string, messages: Message[]): void { this.messages[did] = messages } diff --git a/src/lib/didcomm.ts b/src/lib/didcomm.ts index df84ed6..f82a9da 100644 --- a/src/lib/didcomm.ts +++ b/src/lib/didcomm.ts @@ -8,6 +8,8 @@ import { DIDDoc, SecretsResolver, Secret, + IFromPrior, + FromPrior, Message, UnpackMetadata, PackEncryptedMetadata, @@ -16,8 +18,11 @@ import { Service, } from "didcomm" import DIDPeer from "./peer2" +import * as DIDPeer4 from "./peer4" import { v4 as uuidv4 } from "uuid" import logger from "./logger" +import * as multibase from "multibase" +import * as multicodec from "multicodec" export type DID = string @@ -36,56 +41,119 @@ function x25519ToSecret( return secretEnc } -function ed25519ToSecret( +async function ed25519ToSecret( did: DID, ed25519KeyPriv: Uint8Array, ed25519Key: Uint8Array -): Secret { +): Promise { //const verIdent = DIDPeer.keyToIdent(ed25519Key, "ed25519-pub") const verIdent = "key-1" + const ed25519KeyPriv2 = new Uint8Array(ed25519Key.length + ed25519KeyPriv.length) + ed25519KeyPriv2.set(ed25519KeyPriv) + ed25519KeyPriv2.set(ed25519Key, ed25519KeyPriv.length) const secretVer: Secret = { id: `${did}#${verIdent}`, type: "Ed25519VerificationKey2020", - privateKeyMultibase: DIDPeer.keyToMultibase(ed25519KeyPriv, "ed25519-priv"), + privateKeyMultibase: DIDPeer.keyToMultibase(ed25519KeyPriv2, "ed25519-priv"), } return secretVer } -export function generateDidForMediator() { +export async function generateDidForMediator() { const key = ed25519.utils.randomPrivateKey() const enckeyPriv = edwardsToMontgomeryPriv(key) const verkey = ed25519.getPublicKey(key) const enckey = edwardsToMontgomeryPub(verkey) const service = { type: "DIDCommMessaging", + id: "#service", serviceEndpoint: { uri: "didcomm:transport/queue", accept: ["didcomm/v2"], routingKeys: [] as string[], }, } - const did = DIDPeer.generate([verkey], [enckey], service) + //const did = DIDPeer.generate([verkey], [enckey], service) // did:peer:2 + const doc = { + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/multikey/v1" + ], + "verificationMethod": [ + { + "id": "#key-1", + "type": "Multikey", + "publicKeyMultibase": DIDPeer.keyToMultibase(verkey, "ed25519-pub") + }, + { + "id": "#key-2", + "type": "Multikey", + "publicKeyMultibase": DIDPeer.keyToMultibase(enckey, "x25519-pub") + } + ], + "authentication": [ + "#key-1" + ], + "capabilityDelegation": [ + "#key-1" + ], + "service": [service], + "keyAgreement": [ + "#key-2" + ] + } + const did = await DIDPeer4.encode(doc) - const secretVer = ed25519ToSecret(did, key, verkey) + const secretVer = await ed25519ToSecret(did, key, verkey) const secretEnc = x25519ToSecret(did, enckeyPriv, enckey) return { did, secrets: [secretVer, secretEnc] } } -export function generateDid(routingDid: DID) { +export async function generateDid(routingDid: DID) { const key = ed25519.utils.randomPrivateKey() const enckeyPriv = edwardsToMontgomeryPriv(key) const verkey = ed25519.getPublicKey(key) const enckey = edwardsToMontgomeryPub(verkey) const service = { type: "DIDCommMessaging", + id: "#service", serviceEndpoint: { uri: routingDid, accept: ["didcomm/v2"], }, } - const did = DIDPeer.generate([verkey], [enckey], service) + //const did = DIDPeer.generate([verkey], [enckey], service) // did:peer:2 + const doc = { + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/multikey/v1" + ], + "verificationMethod": [ + { + "id": "#key-1", + "type": "Multikey", + "publicKeyMultibase": DIDPeer.keyToMultibase(verkey, "ed25519-pub") + }, + { + "id": "#key-2", + "type": "Multikey", + "publicKeyMultibase": DIDPeer.keyToMultibase(enckey, "x25519-pub") + } + ], + "authentication": [ + "#key-1" + ], + "capabilityDelegation": [ + "#key-1" + ], + "service": [service], + "keyAgreement": [ + "#key-2" + ] + } + const did = await DIDPeer4.encode(doc) - const secretVer = ed25519ToSecret(did, key, verkey) + const secretVer = await ed25519ToSecret(did, key, verkey) const secretEnc = x25519ToSecret(did, enckeyPriv, enckey) return { did, secrets: [secretVer, secretEnc] } } @@ -103,6 +171,44 @@ export class DIDPeerResolver implements DIDResolver { } } +export class DIDPeer4Resolver implements DIDResolver { + async resolve(did: DID): Promise { + const raw_doc = await DIDPeer4.resolve(did) + const fix_vms = async (vms: Array>) => { + let methods = vms.map((k: Record) => { + let new_method = { + id: `${did}${k.id}`, + type: k.type, + controller: k.controller, + publicKeyMultibase: k.publicKeyMultibase + } + if(new_method.type == "Multikey") { + const key = multibase.decode(k.publicKeyMultibase) + const codec = multicodec.getNameFromData(key) + switch(codec) { + case "x25519-pub": + new_method.type = "X25519KeyAgreementKey2020" + break + case "ed25519-pub": + new_method.type = "Ed25519VerificationKey2020" + break + } + } + return new_method + }) + return methods + }; + const doc = { + id: raw_doc.id, + verificationMethod: await fix_vms(raw_doc.verificationMethod), + authentication: raw_doc.authentication.map((kid: string) => `${raw_doc.id}${kid}`), + keyAgreement: raw_doc.keyAgreement.map((kid: string) => `${raw_doc.id}${kid}`), + service: raw_doc.service, + } + return doc + } +} + var did_web_cache: Record = {}; export class DIDWebResolver implements DIDResolver { @@ -171,6 +277,7 @@ export class PrefixResolver implements DIDResolver { constructor() { this.resolver_map = { "did:peer:2": new DIDPeerResolver() as DIDResolver, + "did:peer:4": new DIDPeer4Resolver() as DIDResolver, "did:web:": new DIDWebResolver() as DIDResolver, } } @@ -304,13 +411,13 @@ export class DIDComm { } async generateDidForMediator(): Promise { - const { did, secrets } = generateDidForMediator() + const { did, secrets } = await generateDidForMediator() secrets.forEach(secret => this.secretsResolver.store_secret(secret)) return did } async generateDid(routingDid: DID): Promise { - const { did, secrets } = generateDid(routingDid) + const { did, secrets } = await generateDid(routingDid) secrets.forEach(secret => this.secretsResolver.store_secret(secret)) return did } @@ -367,6 +474,20 @@ export class DIDComm { } } + async rotateDID( + olddid: DID, + newdid: DID, + ): Promise<[string, string]> { + return await (new FromPrior({iss: olddid, sub: newdid})).pack(null, this.resolver, this.secretsResolver) + return await (new FromPrior({iss: olddid, sub: newdid})).pack(`${olddid}#key-1`, this.resolver, this.secretsResolver) + } + + async getPrior(prior: string): Promise { + const from_prior = await FromPrior.unpack(prior, this.resolver) + console.log("received from_prior:", from_prior) + return from_prior[0].as_value() + } + async prepareMessage( to: DID, from: DID, diff --git a/src/lib/peer4/index.ts b/src/lib/peer4/index.ts new file mode 100644 index 0000000..0ff7208 --- /dev/null +++ b/src/lib/peer4/index.ts @@ -0,0 +1,170 @@ +import * as bs58 from 'bs58'; +import * as varint from 'varint'; + +type Document = Record; + +const json = 0x0200; +const sha2_256 = 0x12; +const sha2_bytes_256 = 0x20; +const base58btc = "z"; +const B58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; +export const LONG_RE = new RegExp(`^did:peer:4zQm[${B58}]{44}:z[${B58}]{6,}$`) +export const SHORT_RE = new RegExp(`^did:peer:4zQm[${B58}]{44}$`) + +function toMultibaseB58(input: Uint8Array): string { + const encoded = bs58.encode(input); + return `${base58btc}${encoded}`; +} + +function fromMultibaseB58(input: string): Uint8Array { + const decoded = bs58.decode(input.slice(1)); + return decoded +} + +async function multihashSha256(input: Uint8Array): Promise { + const mh = new Uint8Array(2); + const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', input)); + varint.encode(sha2_256, mh, 0); + varint.encode(sha2_bytes_256, mh, 1); + const output = new Uint8Array(mh.length + digest.length); + output.set(mh); + output.set(digest, mh.length); + return output +} + +function toMulticodecJson(input: Document): Uint8Array { + const encoded = new TextEncoder().encode(JSON.stringify(input)); + const bytes = new Uint8Array(2 + encoded.length); + varint.encode(json, bytes, 0); + bytes.set(encoded, 2); + return bytes +} + +function fromMulticodecJson(input: Uint8Array): Document { + const decoded = new TextDecoder().decode(input.slice(2)); + return JSON.parse(decoded) +} + +export async function encode(inputDocument: Document): Promise { + const encodedDocument = encodeDocument(inputDocument); + const hash = await hashDocument(encodedDocument); + + const longForm = `did:peer:4${hash}:${encodedDocument}`; + + return longForm; +} + +export async function encodeShort(inputDocument: Document): Promise { + const encodedDocument = encodeDocument(inputDocument); + const hash = await hashDocument(encodedDocument); + + const shortForm = `did:peer:4${hash}`; + + return shortForm; +} + +export function longToShort(did: string): string { + if (!LONG_RE.test(did)) { + throw new Error('DID is not a long form did:peer:4'); + } + + return did.slice(0, did.lastIndexOf(':')) +} + +function encodeDocument(document: Document): string { + const encoded = toMultibaseB58( + toMulticodecJson(document) + ) + return encoded +} + +async function hashDocument(encodedDocument: string): Promise { + const bytes = new TextEncoder().encode(encodedDocument); + const multihashed = await multihashSha256(bytes); + return toMultibaseB58(multihashed); +} + +export async function resolve(did: string): Promise { + const decodedDocument = await decode(did); + const document = contextualizeDocument(did, decodedDocument); + document.alsoKnownAs = document.alsoKnownAs || []; + document.alsoKnownAs.push(longToShort(did)); + return document; +} + +export async function resolveShort(did: string): Promise { + const decodedDocument = await decode(did); + const shortForm = longToShort(did); + const document = contextualizeDocument(shortForm, decodedDocument); + document.alsoKnownAs = document.alsoKnownAs || []; + document.alsoKnownAs.push(did); + return document; +} + +export async function resolveShortFromDoc(document: Document, did: string | null): Promise { + const longForm = await encode(document); + if (did !== null) { + const shortForm = longToShort(longForm); + if (did !== shortForm) { + throw new Error(`DID mismatch: ${did} !== ${shortForm}`); + } + } + return resolveShort(longForm); +} + +export async function decode(did: string): Promise { + if (!did.startsWith("did:peer:4")) { + throw new Error('Invalid did:peer:4'); + } + + if (SHORT_RE.test(did)) { + throw new Error('Cannot decode document form short form did:peer:4'); + } + + if (!LONG_RE.test(did)) { + throw new Error('Invalid did:peer:4'); + } + + const [hash, doc] = did.slice(10).split(':') + if (hash !== await hashDocument(doc)) { + throw new Error(`Hash is invalid for did: ${did}`); + } + + const decoded = fromMulticodecJson(fromMultibaseB58(doc)) + return decoded; +} + +function operateOnEmbedded(callback: (document: Document) => Document): Document | string { + function _curried(document: Document | string): Document | string { + if (typeof document === "string") { + return document; + } else { + return callback(document); + } + } + return _curried; +} + +function visitVerificationMethods(document: Document, callback: (document: Document) => Document) { + document.verificationMethod = document.verificationMethod?.map(callback); + document.authentication = document.authentication?.map(operateOnEmbedded(callback)); + document.assertionMethod = document.assertionMethod?.map(operateOnEmbedded(callback)); + document.keyAgreement = document.keyAgreement?.map(operateOnEmbedded(callback)); + document.capabilityDelegation = document.capabilityDelegation?.map(operateOnEmbedded(callback)); + document.capabilityInvocation = document.capabilityInvocation?.map(operateOnEmbedded(callback)); + return document +} + +function contextualizeDocument(did: string, document: Document): Document { + const contextualized = { ...document }; + contextualized.id = did; + + visitVerificationMethods(contextualized, (vm) => { + if (vm.controller === undefined) { + vm.controller = did + } + return vm + }) + + return contextualized; +} diff --git a/src/lib/worker.ts b/src/lib/worker.ts index d8408fe..27b0e13 100644 --- a/src/lib/worker.ts +++ b/src/lib/worker.ts @@ -8,6 +8,9 @@ const ctx: Worker = self as any class DIDCommWorker { private didcomm: DIDComm private didForMediator: string + private routingDid: string + private mediatorDid: string + private oldDid: string private did: string private ws: WebSocket @@ -25,6 +28,7 @@ class DIDCommWorker { async establishMediation({ mediatorDid }: { mediatorDid: string }) { logger.log("Establishing mediation with mediator: ", mediatorDid) this.didForMediator = await this.didcomm.generateDidForMediator() + logger.log("Generated did: ", this.didForMediator); { const [msg, meta] = await this.didcomm.sendMessageAndExpectReply( mediatorDid, @@ -43,6 +47,8 @@ class DIDCommWorker { } const routingDid = reply.body.routing_did[0] this.did = await this.didcomm.generateDid(routingDid) + this.routingDid = routingDid + this.mediatorDid = mediatorDid this.postMessage({ type: "didGenerated", payload: this.did }) } @@ -84,6 +90,52 @@ class DIDCommWorker { } } + async rotateDid() { + const oldDid = this.did + const did = await this.didcomm.generateDid(this.routingDid) + const rotatedDid = await this.didcomm.rotateDID(oldDid, did) + + const [msg, meta] = await this.didcomm.sendMessageAndExpectReply( + this.mediatorDid, + this.didForMediator, + { + type: "https://didcomm.org/coordinate-mediation/3.0/recipient-update", + body: { + updates: [ + { + recipient_did: did, + action: "add", + }, + ], + }, + } + ) + + const reply = msg.as_value() + if ( + reply.type !== + "https://didcomm.org/coordinate-mediation/3.0/recipient-update-response" + ) { + console.error("Unexpected reply: ", reply) + throw new Error("Unexpected reply") + } + + if (reply.body.updated[0]?.recipient_did !== did) { + throw new Error("Unexpected did in recipient update response") + } + + if (reply.body.updated[0]?.action !== "add") { + throw new Error("Unexpected action in recipient update response") + } + + if (reply.body.updated[0]?.result !== "success") { + throw new Error("Unexpected status in recipient update response") + } + this.did = did + this.oldDid = oldDid + this.postMessage({ type: "didRotated", payload: {newDid: this.did, oldDid: oldDid, jwt: rotatedDid[0]}}) + } + async pickupStatus({ mediatorDid }: { mediatorDid: string }) { const [msg, meta] = await this.didcomm.sendMessageAndExpectReply( mediatorDid, @@ -146,7 +198,7 @@ class DIDCommWorker { } async handleMessage(message: IMessage) { - console.log("handleMessage: ", message) + console.log("handleMessage: ", "(before 'Received:' stringify)", message) switch (message.type) { case "https://didcomm.org/messagepickup/3.0/status": if (message.body.message_count > 0) { @@ -204,6 +256,10 @@ class DIDCommWorker { console.log("Unhandled message: ", message) break } + if("from_prior" in message) { + const prior = await this.didcomm.getPrior(message.from_prior) + message.prior = prior + } this.postMessage({ type: "messageReceived", payload: message }) } diff --git a/src/lib/workerTypes.ts b/src/lib/workerTypes.ts index 6c47216..d75a2f9 100644 --- a/src/lib/workerTypes.ts +++ b/src/lib/workerTypes.ts @@ -1,6 +1,7 @@ export type WorkerCommandType = | "init" | "establishMediation" + | "rotateDid" | "connect" | "disconnect" | "sendMessage" @@ -15,6 +16,7 @@ export type WorkerMessageType = | "init" | "log" | "didGenerated" + | "didRotated" | "messageReceived" | "connected" | "disconnected" diff --git a/src/pages/profile/messaging.ts b/src/pages/profile/messaging.ts index b65acb7..299bf33 100644 --- a/src/pages/profile/messaging.ts +++ b/src/pages/profile/messaging.ts @@ -423,6 +423,13 @@ class MessageHistoryComponent inspectable: false, hideBody: true, }) + case "https://didcomm.org/empty/1.0/empty": + return m(MessageCard, { + header: "Empty", + message, + inspectable: false, + hideBody: true, + }) default: return m( MessageCard, diff --git a/src/pages/profile/navbar.ts b/src/pages/profile/navbar.ts index 3f0e11b..555eda1 100644 --- a/src/pages/profile/navbar.ts +++ b/src/pages/profile/navbar.ts @@ -98,6 +98,11 @@ export default class Navbar implements m.ClassComponent { agent.refreshMessages() } + rotateDid() { + agent.rotateDid() + } + + view(vnode: m.Vnode) { const { profileName, @@ -170,6 +175,17 @@ export default class Navbar implements m.ClassComponent { { style: { marginRight: ".5em" } }, `(${truncatedDid})` ), + did && m( + "button.button.is-white.navbar-item", + { + onclick: () => { + this.rotateDid() + }, + style: { marginRight: ".5em" }, + title: "Rotate DID", + }, + [m("span.icon", [m("i.fas.fa-refresh")])] + ), did && m( "button.button.is-small.is-white",