Merge branch 'develop'

This commit is contained in:
syuilo 2019-07-05 02:13:02 +09:00
commit d078f78602
191 changed files with 4346 additions and 1410 deletions

803
src/@types/jsrsasign.d.ts vendored Normal file
View file

@ -0,0 +1,803 @@
// Attention: Partial Type Definition
declare module 'jsrsasign' {
//// HELPER TYPES
/**
* Attention: The value might be changed by the function.
*/
type Mutable<T> = T;
/**
* Deprecated: The function might be deleted in future release.
*/
type Deprecated<T> = T;
//// COMMON TYPES
/**
* byte number
*/
type ByteNumber = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255;
/**
* hexadecimal string /[0-9A-F]/
*/
type HexString = string;
/**
* binary string /[01]/
*/
type BinString = string;
/**
* base64 string /[A-Za-z0-9+/]=+/
*/
type Base64String = string;
/**
* base64 URL encoded string /[A-Za-z0-9_-]/
*/
type Base64URLString = string;
/**
* time value (ex. "151231235959Z")
*/
type TimeValue = string;
/**
* OID string (ex. '1.2.3.4.567')
*/
type OID = string;
/**
* OID name
*/
type OIDName = string;
/**
* PEM formatted string
*/
type PEM = string;
//// ASN1 TYPES
class ASN1Object {
public isModified: boolean;
public hTLV: ASN1TLV;
public hT: ASN1T;
public hL: ASN1L;
public hV: ASN1V;
public getLengthHexFromValue(): HexString;
public getEncodedHex(): ASN1TLV;
public getValueHex(): ASN1V;
public getFreshValueHex(): ASN1V;
}
class DERAbstractStructured extends ASN1Object {
constructor(params?: Partial<Record<'array', ASN1Object[]>>);
public setByASN1ObjectArray(asn1ObjectArray: ASN1Object[]): void;
public appendASN1Object(asn1Object: ASN1Object): void;
}
class DERSequence extends DERAbstractStructured {
constructor(params?: Partial<Record<'array', ASN1Object[]>>);
public getFreshValueHex(): ASN1V;
}
//// ASN1HEX TYPES
/**
* ASN.1 DER encoded data (hexadecimal string)
*/
type ASN1S = HexString;
/**
* index of something
*/
type Idx<T extends { [idx: string]: unknown } | { [idx: number]: unknown }> = ASN1S extends { [idx: string]: unknown } ? string : ASN1S extends { [idx: number]: unknown } ? number : never;
/**
* byte length of something
*/
type ByteLength<T extends { length: unknown }> = T['length'];
/**
* ASN.1 L(length) (hexadecimal string)
*/
type ASN1L = HexString;
/**
* ASN.1 T(tag) (hexadecimal string)
*/
type ASN1T = HexString;
/**
* ASN.1 V(value) (hexadecimal string)
*/
type ASN1V = HexString;
/**
* ASN.1 TLV (hexadecimal string)
*/
type ASN1TLV = HexString;
/**
* ASN.1 object string
*/
type ASN1ObjectString = string;
/**
* nth
*/
type Nth = number;
/**
* ASN.1 DER encoded OID value (hexadecimal string)
*/
type ASN1OIDV = HexString;
class ASN1HEX {
public static getLblen(s: ASN1S, idx: Idx<ASN1S>): ByteLength<ASN1L>;
public static getL(s: ASN1S, idx: Idx<ASN1S>): ASN1L;
public static getVblen(s: ASN1S, idx: Idx<ASN1S>): ByteLength<ASN1V>;
public static getVidx(s: ASN1S, idx: Idx<ASN1S>): Idx<ASN1V>;
public static getV(s: ASN1S, idx: Idx<ASN1S>): ASN1V;
public static getTLV(s: ASN1S, idx: Idx<ASN1S>): ASN1TLV;
public static getNextSiblingIdx(s: ASN1S, idx: Idx<ASN1S>): Idx<ASN1ObjectString>;
public static getChildIdx(h: ASN1S, pos: Idx<ASN1S>): Idx<ASN1ObjectString>[];
public static getNthChildIdx(h: ASN1S, idx: Idx<ASN1S>, nth: Nth): Idx<ASN1ObjectString>;
public static getIdxbyList(h: ASN1S, currentIndex: Idx<ASN1ObjectString>, nthList: Mutable<Nth[]>, checkingTag?: string): Idx<Mutable<Nth[]>>;
public static getTLVbyList(h: ASN1S, currentIndex: Idx<ASN1ObjectString>, nthList: Mutable<Nth[]>, checkingTag?: string): ASN1TLV;
public static getVbyList(h: ASN1S, currentIndex: Idx<ASN1ObjectString>, nthList: Mutable<Nth[]>, checkingTag?: string, removeUnusedbits?: boolean): ASN1V;
public static hextooidstr(hex: ASN1OIDV): OID;
public static dump(hexOrObj: ASN1S | ASN1Object, flags?: Record<string, unknown>, idx?: Idx<ASN1S>, indent?: string): string;
public static isASN1HEX(hex: string): hex is HexString;
public static oidname(oidDotOrHex: OID | ASN1OIDV): OIDName;
}
//// BIG INTEGER TYPES (PARTIAL)
class BigInteger {
constructor(a: null);
constructor(a: number, b: SecureRandom);
constructor(a: number, b: number, c: SecureRandom);
constructor(a: unknown);
constructor(a: string, b: number);
public am(i: number, x: number, w: number, j: number, c: number, n: number): number;
public DB: number;
public DM: number;
public DV: number;
public FV: number;
public F1: number;
public F2: number;
protected copyTo(r: Mutable<BigInteger>): void;
protected fromInt(x: number): void;
protected fromString(s: string, b: number): void;
protected clamp(): void;
public toString(b: number): string;
public negate(): BigInteger;
public abs(): BigInteger;
public compareTo(a: BigInteger): number;
public bitLength(): number;
protected dlShiftTo(n: number, r: Mutable<BigInteger>): void;
protected drShiftTo(n: number, r: Mutable<BigInteger>): void;
protected lShiftTo(n: number, r: Mutable<BigInteger>): void;
protected rShiftTo(n: number, r: Mutable<BigInteger>): void;
protected subTo(a: BigInteger, r: Mutable<BigInteger>): void;
protected multiplyTo(a: BigInteger, r: Mutable<BigInteger>): void;
protected squareTo(r: Mutable<BigInteger>): void;
protected divRemTo(m: BigInteger, q: Mutable<BigInteger>, r: Mutable<BigInteger>): void;
public mod(a: BigInteger): BigInteger;
protected invDigit(): number;
protected isEven(): boolean;
protected exp(e: number, z: Classic | Montgomery): BigInteger;
public modPowInt(e: number, m: BigInteger): BigInteger;
public static ZERO: BigInteger;
public static ONE: BigInteger;
}
class Classic {
constructor(m: BigInteger);
public convert(x: BigInteger): BigInteger;
public revert(x: BigInteger): BigInteger;
public reduce(x: Mutable<BigInteger>): void;
public mulTo(x: BigInteger, r: Mutable<BigInteger>): void;
public sqrTo(x: BigInteger, y: BigInteger, r: Mutable<BigInteger>): void;
}
class Montgomery {
constructor(m: BigInteger);
public convert(x: BigInteger): BigInteger;
public revert(x: BigInteger): BigInteger;
public reduce(x: Mutable<BigInteger>): void;
public mulTo(x: BigInteger, r: Mutable<BigInteger>): void;
public sqrTo(x: BigInteger, y: BigInteger, r: Mutable<BigInteger>): void;
}
//// KEYUTIL TYPES
type DecryptAES = (dataHex: HexString, keyHex: HexString, ivHex: HexString) => HexString;
type Decrypt3DES = (dataHex: HexString, keyHex: HexString, ivHex: HexString) => HexString;
type DecryptDES = (dataHex: HexString, keyHex: HexString, ivHex: HexString) => HexString;
type EncryptAES = (dataHex: HexString, keyHex: HexString, ivHex: HexString) => HexString;
type Encrypt3DES = (dataHex: HexString, keyHex: HexString, ivHex: HexString) => HexString;
type EncryptDES = (dataHex: HexString, keyHex: HexString, ivHex: HexString) => HexString;
type AlgList = {
'AES-256-CBC': { 'proc': DecryptAES; 'eproc': EncryptAES; keylen: 32; ivlen: 16; };
'AES-192-CBC': { 'proc': DecryptAES; 'eproc': EncryptAES; keylen: 24; ivlen: 16; };
'AES-128-CBC': { 'proc': DecryptAES; 'eproc': EncryptAES; keylen: 16; ivlen: 16; };
'DES-EDE3-CBC': { 'proc': Decrypt3DES; 'eproc': Encrypt3DES; keylen: 24; ivlen: 8; };
'DES-CBC': { 'proc': DecryptDES; 'eproc': EncryptDES; keylen: 8; ivlen: 8; };
};
type AlgName = keyof AlgList;
type PEMHeadAlgName = 'RSA' | 'EC' | 'DSA';
type GetKeyRSAParam = RSAKey | {
n: BigInteger;
e: number;
} | Record<'n' | 'e', HexString> | Record<'n' | 'e', HexString> & Record<'d' | 'p' | 'q' | 'dp' | 'dq' | 'co', HexString | null> | {
n: BigInteger;
e: number;
d: BigInteger;
} | {
kty: 'RSA';
} & Record<'n' | 'e', Base64URLString> | {
kty: 'RSA';
} & Record<'n' | 'e' | 'd' | 'p' | 'q' | 'dp' | 'dq' | 'qi', Base64URLString> | {
kty: 'RSA';
} & Record<'n' | 'e' | 'd', Base64URLString>;
type GetKeyECDSAParam = KJUR.crypto.ECDSA | {
curve: KJUR.crypto.CurveName;
xy: HexString;
} | {
curve: KJUR.crypto.CurveName;
d: HexString;
} | {
kty: 'EC';
crv: KJUR.crypto.CurveName;
x: Base64URLString;
y: Base64URLString;
} | {
kty: 'EC';
crv: KJUR.crypto.CurveName;
x: Base64URLString;
y: Base64URLString;
d: Base64URLString;
};
type GetKeyDSAParam = KJUR.crypto.DSA | Record<'p' | 'q' | 'g', BigInteger> & Record<'y', BigInteger | null> | Record<'p' | 'q' | 'g' | 'x', BigInteger> & Record<'y', BigInteger | null>;
type GetKeyParam = GetKeyRSAParam | GetKeyECDSAParam | GetKeyDSAParam | string;
class KEYUTIL {
public version: '1.0.0';
public parsePKCS5PEM(sPKCS5PEM: PEM): Partial<Record<'type' | 's', string>> & (Record<'cipher' | 'ivsalt', string> | Record<'cipher' | 'ivsalt', undefined>);
public getKeyAndUnusedIvByPasscodeAndIvsalt(algName: AlgName, passcode: string, ivsaltHex: HexString): Record<'keyhex' | 'ivhex', HexString>;
public decryptKeyB64(privateKeyB64: Base64String, sharedKeyAlgName: AlgName, sharedKeyHex: HexString, ivsaltHex: HexString): Base64String;
public getDecryptedKeyHex(sEncryptedPEM: PEM, passcode: string): HexString;
public getEncryptedPKCS5PEMFromPrvKeyHex(pemHeadAlg: PEMHeadAlgName, hPrvKey: string, passcode: string, sharedKeyAlgName?: AlgName | null, ivsaltHex?: HexString | null): PEM;
public parseHexOfEncryptedPKCS8(sHEX: HexString): {
ciphertext: ASN1V;
encryptionSchemeAlg: 'TripleDES';
encryptionSchemeIV: ASN1V;
pbkdf2Salt: ASN1V;
pbkdf2Iter: number;
};
public getPBKDF2KeyHexFromParam(info: ReturnType<this['parseHexOfEncryptedPKCS8']>, passcode: string): HexString;
private _getPlainPKCS8HexFromEncryptedPKCS8PEM(pkcs8PEM: PEM, passcode: string): HexString;
public getKeyFromEncryptedPKCS8PEM(prvKeyHex: HexString): ReturnType<this['getKeyFromPlainPrivatePKCS8Hex']>;
public parsePlainPrivatePKCS8Hex(pkcs8PrvHex: HexString): {
algparam: ASN1V | null;
algoid: ASN1V;
keyidx: Idx<ASN1V>;
};
public getKeyFromPlainPrivatePKCS8PEM(prvKeyHex: HexString): ReturnType<this['getKeyFromPlainPrivatePKCS8Hex']>;
public getKeyFromPlainPrivatePKCS8Hex(prvKeyHex: HexString): RSAKey | KJUR.crypto.DSA | KJUR.crypto.ECDSA;
private _getKeyFromPublicPKCS8Hex(h: HexString): RSAKey | KJUR.crypto.DSA | KJUR.crypto.ECDSA;
public parsePublicRawRSAKeyHex(pubRawRSAHex: HexString): Record<'n' | 'e', ASN1V>;
public parsePublicPKCS8Hex(pkcs8PubHex: HexString): {
algparam: ASN1V | Record<'p' | 'q' | 'g', ASN1V> | null;
algoid: ASN1V;
key: ASN1V;
};
public static getKey(param: GetKeyRSAParam): RSAKey;
public static getKey(param: GetKeyECDSAParam): KJUR.crypto.ECDSA;
public static getKey(param: GetKeyDSAParam): KJUR.crypto.DSA;
public static getKey(param: string, passcode?: string, hextype?: string): RSAKey | KJUR.crypto.ECDSA | KJUR.crypto.DSA;
public static generateKeypair(alg: 'RSA', keylen: number): Record<'prvKeyObj' | 'pubKeyObj', RSAKey>;
public static generateKeypair(alg: 'EC', curve: KJUR.crypto.CurveName): Record<'prvKeyObj' | 'pubKeyObj', KJUR.crypto.ECDSA>;
public static getPEM(keyObjOrHex: RSAKey | KJUR.crypto.ECDSA | KJUR.crypto.DSA, formatType?: 'PKCS1PRV' | 'PKCS5PRV' | 'PKCS8PRV', passwd?: string, encAlg?: 'DES-CBC' | 'DES-EDE3-CBC' | 'AES-128-CBC' | 'AES-192-CBC' | 'AES-256-CBC', hexType?: string, ivsaltHex?: HexString): object; // To Do
public static getKeyFromCSRPEM(csrPEM: PEM): RSAKey | KJUR.crypto.ECDSA | KJUR.crypto.DSA;
public static getKeyFromCSRHex(csrHex: HexString): RSAKey | KJUR.crypto.ECDSA | KJUR.crypto.DSA;
public static parseCSRHex(csrHex: HexString): Record<'p8pubkeyhex', ASN1TLV>;
public static getJWKFromKey(keyObj: RSAKey): {
kty: 'RSA';
} & Record<'n' | 'e' | 'd' | 'p' | 'q' | 'dp' | 'dq' | 'qi', Base64URLString> | {
kty: 'RSA';
} & Record<'n' | 'e', Base64URLString>;
public static getJWKFromKey(keyObj: KJUR.crypto.ECDSA): {
kty: 'EC';
crv: KJUR.crypto.CurveName;
x: Base64URLString;
y: Base64URLString;
d: Base64URLString;
} | {
kty: 'EC';
crv: KJUR.crypto.CurveName;
x: Base64URLString;
y: Base64URLString;
};
}
//// KJUR NAMESPACE (PARTIAL)
namespace KJUR {
namespace crypto {
type CurveName = 'secp128r1' | 'secp160k1' | 'secp160r1' | 'secp192k1' | 'secp192r1' | 'secp224r1' | 'secp256k1' | 'secp256r1' | 'secp384r1' | 'secp521r1';
class DSA {
public p: BigInteger | null;
public q: BigInteger | null;
public g: BigInteger | null;
public y: BigInteger | null;
public x: BigInteger | null;
public type: 'DSA';
public isPrivate: boolean;
public isPublic: boolean;
public setPrivate(p: BigInteger, q: BigInteger, g: BigInteger, y: BigInteger | null, x: BigInteger): void;
public setPrivateHex(hP: HexString, hQ: HexString, hG: HexString, hY: HexString | null, hX: HexString): void;
public setPublic(p: BigInteger, q: BigInteger, g: BigInteger, y: BigInteger): void;
public setPublicHex(hP: HexString, hQ: HexString, hG: HexString, hY: HexString): void;
public signWithMessageHash(sHashHex: HexString): HexString;
public verifyWithMessageHash(sHashHex: HexString, hSigVal: HexString): boolean;
public parseASN1Signature(hSigVal: HexString): [BigInteger, BigInteger];
public readPKCS5PrvKeyHex(h: HexString): void;
public readPKCS8PrvKeyHex(h: HexString): void;
public readPKCS8PubKeyHex(h: HexString): void;
public readCertPubKeyHex(h: HexString, nthPKI: number): void;
}
class ECDSA {
constructor(params?: {
curve?: CurveName;
prv?: HexString;
pub?: HexString;
});
public p: BigInteger | null;
public q: BigInteger | null;
public g: BigInteger | null;
public y: BigInteger | null;
public x: BigInteger | null;
public type: 'EC';
public isPrivate: boolean;
public isPublic: boolean;
public getBigRandom(limit: BigInteger): BigInteger;
public setNamedCurve(curveName: CurveName): void;
public setPrivateKeyHex(prvKeyHex: HexString): void;
public setPublicKeyHex(pubKeyHex: HexString): void;
public getPublicKeyXYHex(): Record<'x' | 'y', HexString>;
public getShortNISTPCurveName(): 'P-256' | 'P-384' | null;
public generateKeyPairHex(): Record<'ecprvhex' | 'ecpubhex', HexString>;
public signWithMessageHash(hashHex: HexString): HexString;
public signHex(hashHex: HexString, privHex: HexString): HexString;
public verifyWithMessageHash(sHashHex: HexString, hSigVal: HexString): boolean;
public parseASN1Signature(hSigVal: HexString): [BigInteger, BigInteger];
public readPKCS5PrvKeyHex(h: HexString): void;
public readPKCS8PrvKeyHex(h: HexString): void;
public readPKCS8PubKeyHex(h: HexString): void;
public readCertPubKeyHex(h: HexString, nthPKI: number): void;
public static parseSigHex(sigHex: HexString): Record<'r' | 's', BigInteger>;
public static parseSigHexInHexRS(sigHex: HexString): Record<'r' | 's', ASN1V>;
public static asn1SigToConcatSig(asn1Sig: HexString): HexString;
public static concatSigToASN1Sig(concatSig: HexString): ASN1TLV;
public static hexRSSigToASN1Sig(hR: HexString, hS: HexString): ASN1TLV;
public static biRSSigToASN1Sig(biR: BigInteger, biS: BigInteger): ASN1TLV;
public static getName(s: CurveName | HexString): 'secp256r1' | 'secp256k1' | 'secp384r1' | null;
}
class Signature {
constructor(params?: ({
alg: string;
prov?: string;
} | {}) & ({
psssaltlen: number;
} | {}) & ({
prvkeypem: PEM;
prvkeypas?: never;
} | {}));
private _setAlgNames(): void;
private _zeroPaddingOfSignature(hex: HexString, bitLength: number): HexString;
public setAlgAndProvider(alg: string, prov: string): void;
public init(key: GetKeyParam, pass?: string): void;
public updateString(str: string): void;
public updateHex(hex: HexString): void;
public sign(): HexString;
public signString(str: string): HexString;
public signHex(hex: HexString): HexString;
public verify(hSigVal: string): boolean | 0;
}
}
}
//// RSAKEY TYPES
class RSAKey {
public n: BigInteger | null;
public e: number;
public d: BigInteger | null;
public p: BigInteger | null;
public q: BigInteger | null;
public dmp1: BigInteger | null;
public dmq1: BigInteger | null;
public coeff: BigInteger | null;
public type: 'RSA';
public isPrivate?: boolean;
public isPublic?: boolean;
//// RSA PUBLIC
protected doPublic(x: BigInteger): BigInteger;
public setPublic(N: BigInteger, E: number): void;
public setPublic(N: HexString, E: HexString): void;
public encrypt(text: string): HexString | null;
public encryptOAEP(text: string, hash?: string, hashLen?: number): HexString | null;
public encryptOAEP(text: string, hash?: (s: string) => string, hashLen?: number): HexString | null;
//// RSA PRIVATE
protected doPrivate(x: BigInteger): BigInteger;
public setPrivate(N: BigInteger, E: number, D: BigInteger): void;
public setPrivate(N: HexString, E: HexString, D: HexString): void;
public setPrivateEx(N: HexString, E: HexString, D?: HexString | null, P?: HexString | null, Q?: HexString | null, DP?: HexString | null, DQ?: HexString | null, C?: HexString | null): void;
public generate(B: number, E: HexString): void;
public decrypt(ctext: HexString): string;
public decryptOAEP(ctext: HexString, hash?: string, hashLen?: number): string | null;
public encryptOAEP(ctext: HexString, hash?: (s: string) => string, hashLen?: number): string | null;
//// RSA PEM
public getPosArrayOfChildrenFromHex(hPrivateKey: PEM): Idx<ASN1ObjectString>[];
public getHexValueArrayOfChildrenFromHex(hPrivateKey: PEM): Idx<ASN1ObjectString>[];
public readPrivateKeyFromPEMString(keyPEM: PEM): void;
public readPKCS5PrvKeyHex(h: HexString): void;
public readPKCS8PrvKeyHex(h: HexString): void;
public readPKCS5PubKeyHex(h: HexString): void;
public readPKCS8PubKeyHex(h: HexString): void;
public readCertPubKeyHex(h: HexString, nthPKI: Nth): void;
//// RSA SIGN
public sign(s: string, hashAlg: string): HexString;
public signWithMessageHash(sHashHex: HexString, hashAlg: string): HexString;
public signPSS(s: string, hashAlg: string, sLen: number): HexString;
public signWithMessageHashPSS(hHash: HexString, hashAlg: string, sLen: number): HexString;
public verify(sMsg: string, hSig: HexString): boolean | 0;
public verifyWithMessageHash(sHashHex: HexString, hSig: HexString): boolean | 0;
public verifyPSS(sMsg: string, hSig: HexString, hashAlg: string, sLen: number): boolean;
public verifyWithMessageHashPSS(hHash: HexString, hSig: HexString, hashAlg: string, sLen: number): boolean;
public static SALT_LEN_HLEN: -1;
public static SALT_LEN_MAX: -2;
public static SALT_LEN_RECOVER: -2;
}
/// RNG TYPES
class SecureRandom {
public nextBytes(ba: Mutable<ByteNumber[]>): void;
}
//// X509 TYPES
type ExtInfo = {
critical: boolean;
oid: OID;
vidx: Idx<ASN1V>;
};
type ExtAIAInfo = Record<'ocsp' | 'caissuer', string>;
type ExtCertificatePolicy = {
id: OIDName;
} & Partial<{
cps: string;
} | {
unotice: string;
}>;
class X509 {
public hex: HexString | null;
public version: number;
public foffset: number;
public aExtInfo: null;
public getVersion(): number;
public getSerialNumberHex(): ASN1V;
public getSignatureAlgorithmField(): OIDName;
public getIssuerHex(): ASN1TLV;
public getIssuerString(): HexString;
public getSubjectHex(): ASN1TLV;
public getSubjectString(): HexString;
public getNotBefore(): TimeValue;
public getNotAfter(): TimeValue;
public getPublicKeyHex(): ASN1TLV;
public getPublicKeyIdx(): Idx<Mutable<Nth[]>>;
public getPublicKeyContentIdx(): Idx<Mutable<Nth[]>>;
public getPublicKey(): RSAKey | KJUR.crypto.ECDSA | KJUR.crypto.DSA;
public getSignatureAlgorithmName(): OIDName;
public getSignatureValueHex(): ASN1V;
public verifySignature(pubKey: GetKeyParam): boolean | 0;
public parseExt(): void;
public getExtInfo(oidOrName: OID | string): ExtInfo | undefined;
public getExtBasicConstraints(): ExtInfo | {} | {
cA: true;
pathLen?: number;
};
public getExtKeyUsageBin(): BinString;
public getExtKeyUsageString(): string;
public getExtSubjectKeyIdentifier(): ASN1V | undefined;
public getExtAuthorityKeyIdentifier(): {
kid: ASN1V;
} | undefined;
public getExtExtKeyUsageName(): OIDName[] | undefined;
public getExtSubjectAltName(): Deprecated<string[]>;
public getExtSubjectAltName2(): ['MAIL' | 'DNS' | 'DN' | 'URI' | 'IP', string][] | undefined;
public getExtCRLDistributionPointsURI(): string[] | undefined;
public getExtAIAInfo(): ExtAIAInfo | undefined;
public getExtCertificatePolicies(): ExtCertificatePolicy[] | undefined;
public readCertPEM(sCertPEM: PEM): void;
public readCertHex(sCertHex: HexString): void;
public getInfo(): string;
public static hex2dn(hex: HexString, idx?: Idx<HexString>): string;
public static hex2rdn(hex: HexString, idx?: Idx<HexString>): string;
public static hex2attrTypeValue(hex: HexString, idx?: Idx<HexString>): string;
public static getPublicKeyFromCertPEM(sCertPEM: PEM): RSAKey | KJUR.crypto.ECDSA | KJUR.crypto.DSA;
public static getPublicKeyInfoPropOfCertPEM(sCertPEM: PEM): {
algparam: ASN1V | null;
leyhex: ASN1V;
algoid: ASN1V;
};
}
}

View file

@ -79,6 +79,7 @@ export async function masterMain() {
require('../daemons/server-stats').default();
require('../daemons/notes-stats').default();
require('../daemons/queue-stats').default();
require('../daemons/janitor').default();
}
bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, null, true);

View file

@ -5,6 +5,14 @@
<section v-if="tables">
<div v-for="table in Object.keys(tables)"><b>{{ table }}</b> {{ tables[table].count | number }} {{ tables[table].size | bytes }}</div>
</section>
<section>
<header><fa :icon="faBroom"/> {{ $t('vacuum') }}</header>
<ui-info>{{ $t('vacuum-info') }}</ui-info>
<ui-switch v-model="fullVacuum">FULL</ui-switch>
<ui-switch v-model="analyzeVacuum">ANALYZE</ui-switch>
<ui-button @click="vacuum()"><fa :icon="faBroom"/> {{ $t('vacuum') }}</ui-button>
<ui-info warn>{{ $t('vacuum-exclamation') }}</ui-info>
</section>
</ui-card>
</div>
</template>
@ -12,7 +20,7 @@
<script lang="ts">
import Vue from 'vue';
import i18n from '../../i18n';
import { faDatabase } from '@fortawesome/free-solid-svg-icons';
import { faDatabase, faBroom } from '@fortawesome/free-solid-svg-icons';
export default Vue.extend({
i18n: i18n('admin/views/db.vue'),
@ -20,7 +28,9 @@ export default Vue.extend({
data() {
return {
tables: null,
faDatabase
fullVacuum: true,
analyzeVacuum: true,
faDatabase, faBroom
};
},
@ -34,6 +44,18 @@ export default Vue.extend({
this.tables = tables;
});
},
vacuum() {
this.$root.api('admin/vacuum', {
full: this.fullVacuum,
analyze: this.analyzeVacuum,
}).then(() => {
this.$root.dialog({
type: 'success',
splash: true
});
});
},
}
});
</script>

View file

@ -129,6 +129,7 @@
<ui-input v-model="smtpPass" type="password" :with-password-toggle="true" :disabled="!enableEmail || !smtpAuth">{{ $t('smtp-pass') }}</ui-input>
</ui-horizon-group>
<ui-switch v-model="smtpSecure" :disabled="!enableEmail">{{ $t('smtp-secure') }}<template #desc>{{ $t('smtp-secure-info') }}</template></ui-switch>
<ui-button @click="testEmail()">{{ $t('test-email') }}</ui-button>
</template>
</section>
<section>
@ -424,6 +425,24 @@ export default Vue.extend({
});
},
async testEmail() {
this.$root.api('admin/send-email', {
to: this.maintainerEmail,
subject: 'Test email',
text: 'Yo'
}).then(x => {
this.$root.dialog({
type: 'success',
splash: true
});
}).catch(e => {
this.$root.dialog({
type: 'error',
text: e
});
});
},
updateMeta() {
this.$root.api('admin/update-meta', {
maintainerName: this.maintainerName,

View file

@ -41,7 +41,6 @@
if (`${url.pathname}/`.startsWith('/dev/')) app = 'dev';
if (`${url.pathname}/`.startsWith('/auth/')) app = 'auth';
if (`${url.pathname}/`.startsWith('/admin/')) app = 'admin';
if (`${url.pathname}/`.startsWith('/test/')) app = 'test';
//#endregion
// Script version

View file

@ -0,0 +1,5 @@
export function hexifyAB(buffer) {
return Array.from(new Uint8Array(buffer))
.map(item => item.toString(16).padStart(2, 0))
.join('');
}

View file

@ -33,7 +33,7 @@
</template>
</ui-select>
<ui-horizon-group no-grow class="buttons fit-bottom" v-if="!splash && (showOkButton || showCancelButton)">
<ui-button @click="ok" v-if="showOkButton" primary :autofocus="!input && !select && !user">{{ (showCancelButton || input || select || user) ? $t('@.ok') : $t('@.got-it') }}</ui-button>
<ui-button @click="ok" v-if="showOkButton" primary :autofocus="!input && !select && !user" :disabled="!canOk">{{ (showCancelButton || input || select || user) ? $t('@.ok') : $t('@.got-it') }}</ui-button>
<ui-button @click="cancel" v-if="showCancelButton || input || select || user">{{ $t('@.cancel') }}</ui-button>
</ui-horizon-group>
</template>
@ -99,11 +99,26 @@ export default Vue.extend({
inputValue: this.input && this.input.default ? this.input.default : null,
userInputValue: null,
selectedValue: this.select ? this.select.items ? this.select.items[0].value : this.select.groupedItems[0].items[0].value : null,
canOk: true,
faTimesCircle, faQuestionCircle
};
},
watch: {
userInputValue() {
if (this.user) {
this.$root.api('users/show', parseAcct(this.userInputValue)).then(u => {
this.canOk = u != null;
}).catch(() => {
this.canOk = false;
});
}
}
},
mounted() {
if (this.user) this.canOk = false;
this.$nextTick(() => {
(this.$refs.bg as any).style.pointerEvents = 'auto';
anime({
@ -131,6 +146,7 @@ export default Vue.extend({
methods: {
async ok() {
if (!this.canOk) return;
if (!this.showOkButton) return;
if (this.user) {

View file

@ -92,6 +92,14 @@ export default Vue.extend({
try {
if (this.isFollowing) {
const { canceled } = await this.$root.dialog({
type: 'warning',
text: this.$t('@.unfollow-confirm', { name: this.user.name || this.user.username }),
showCancelButton: true
});
if (canceled) return;
await this.$root.api('following/delete', {
userId: this.user.id
});

View file

@ -15,6 +15,8 @@ export default Vue.extend({
<style lang="stylus" scoped>
.havbbuyv
white-space pre-wrap
>>> .title
display block
margin-bottom 4px

View file

@ -9,7 +9,6 @@ import Vue from 'vue';
import i18n from '../../../i18n';
import { url } from '../../../config';
import copyToClipboard from '../../../common/scripts/copy-to-clipboard';
import { concat, intersperse } from '../../../../../prelude/array';
import { faCopy, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons';
export default Vue.extend({
@ -129,6 +128,13 @@ export default Vue.extend({
splash: true
});
this.destroyDom();
}).catch(e => {
if (e.id === '72dab508-c64d-498f-8740-a8eec1ba385a') {
this.$root.dialog({
type: 'error',
text: this.$t('pin-limit-exceeded')
});
}
});
},

View file

@ -1,11 +1,54 @@
<template>
<div class="2fa">
<div class="2fa totp-section">
<p style="margin-top:0;">{{ $t('intro') }}<a :href="$t('url')" target="_blank">{{ $t('detail') }}</a></p>
<ui-info warn>{{ $t('caution') }}</ui-info>
<p v-if="!data && !$store.state.i.twoFactorEnabled"><ui-button @click="register">{{ $t('register') }}</ui-button></p>
<template v-if="$store.state.i.twoFactorEnabled">
<h2 class="heading">{{ $t('totp-header') }}</h2>
<p>{{ $t('already-registered') }}</p>
<ui-button @click="unregister">{{ $t('unregister') }}</ui-button>
<template v-if="supportsCredentials">
<hr class="totp-method-sep">
<h2 class="heading">{{ $t('security-key-header') }}</h2>
<p>{{ $t('security-key') }}</p>
<div class="key-list">
<div class="key" v-for="key in $store.state.i.securityKeysList">
<h3>
{{ key.name }}
</h3>
<div class="last-used">
{{ $t('last-used') }}
<mk-time :time="key.lastUsed"/>
</div>
<ui-button @click="unregisterKey(key)">
{{ $t('unregister') }}
</ui-button>
</div>
</div>
<ui-info warn v-if="registration && registration.error">{{ $t('something-went-wrong') }} {{ registration.error }}</ui-info>
<ui-button v-if="!registration || registration.error" @click="addSecurityKey">{{ $t('register') }}</ui-button>
<ol v-if="registration && !registration.error">
<li v-if="registration.stage >= 0">
{{ $t('activate-key') }}
<fa icon="spinner" pulse fixed-width v-if="registration.saving && registration.stage == 0" />
</li>
<li v-if="registration.stage >= 1">
<ui-form :disabled="registration.stage != 1 || registration.saving">
<ui-input v-model="keyName" :max="30">
<span>{{ $t('security-key-name') }}</span>
</ui-input>
<ui-button @click="registerKey" :disabled="this.keyName.length == 0">
{{ $t('register-security-key') }}
</ui-button>
<fa icon="spinner" pulse fixed-width v-if="registration.saving && registration.stage == 1" />
</ui-form>
</li>
</ol>
</template>
</template>
<div v-if="data && !$store.state.i.twoFactorEnabled">
<ol>
@ -24,12 +67,21 @@
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../../i18n';
import { hostname } from '../../../../config';
import { hexifyAB } from '../../../scripts/2fa';
function stringifyAB(buffer) {
return String.fromCharCode.apply(null, new Uint8Array(buffer));
}
export default Vue.extend({
i18n: i18n('desktop/views/components/settings.2fa.vue'),
data() {
return {
data: null,
supportsCredentials: !!navigator.credentials,
registration: null,
keyName: '',
token: null
};
},
@ -76,7 +128,116 @@ export default Vue.extend({
}).catch(() => {
this.$notify(this.$t('failed'));
});
},
registerKey() {
this.registration.saving = true;
this.$root.api('i/2fa/key-done', {
password: this.registration.password,
name: this.keyName,
challengeId: this.registration.challengeId,
// we convert each 16 bits to a string to serialise
clientDataJSON: stringifyAB(this.registration.credential.response.clientDataJSON),
attestationObject: hexifyAB(this.registration.credential.response.attestationObject)
}).then(key => {
this.registration = null;
key.lastUsed = new Date();
this.$notify(this.$t('success'));
})
},
unregisterKey(key) {
this.$root.dialog({
title: this.$t('enter-password'),
input: {
type: 'password'
}
}).then(({ canceled, result: password }) => {
if (canceled) return;
return this.$root.api('i/2fa/remove-key', {
password,
credentialId: key.id
}).then(() => {
this.$notify(this.$t('key-unregistered'));
});
});
},
addSecurityKey() {
this.$root.dialog({
title: this.$t('enter-password'),
input: {
type: 'password'
}
}).then(({ canceled, result: password }) => {
if (canceled) return;
this.$root.api('i/2fa/register-key', {
password
}).then(registration => {
this.registration = {
password,
challengeId: registration.challengeId,
stage: 0,
publicKeyOptions: {
challenge: Buffer.from(
registration.challenge
.replace(/\-/g, "+")
.replace(/_/g, "/"),
'base64'
),
rp: {
id: hostname,
name: 'Misskey'
},
user: {
id: Uint8Array.from(this.$store.state.i.id, c => c.charCodeAt(0)),
name: this.$store.state.i.username,
displayName: this.$store.state.i.name,
},
pubKeyCredParams: [{alg: -7, type: 'public-key'}],
timeout: 60000,
attestation: 'direct'
},
saving: true
};
return navigator.credentials.create({
publicKey: this.registration.publicKeyOptions
});
}).then(credential => {
this.registration.credential = credential;
this.registration.saving = false;
this.registration.stage = 1;
}).catch(err => {
console.warn('Error while registering?', err);
this.registration.error = err.message;
this.registration.stage = -1;
});
});
}
}
});
</script>
<style lang="stylus" scoped>
.totp-section
.totp-method-sep
margin 1.5em 0 1em
border none
border-top solid var(--lineWidth) var(--faceDivider)
h2.heading
margin 0
.key
padding 1em
margin 0.5em 0
background #161616
border-radius 6px
h3
margin-top 0
margin-bottom .3em
.last-used
margin-bottom .5em
</style>

View file

@ -1,23 +1,40 @@
<template>
<form class="mk-signin" :class="{ signing }" @submit.prevent="onSubmit">
<form class="mk-signin" :class="{ signing, totpLogin }" @submit.prevent="onSubmit">
<div class="avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : null }" v-show="withAvatar"></div>
<ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required @input="onUsernameChange">
<span>{{ $t('username') }}</span>
<template #prefix>@</template>
<template #suffix>@{{ host }}</template>
</ui-input>
<ui-input v-model="password" type="password" :with-password-toggle="true" required>
<span>{{ $t('password') }}</span>
<template #prefix><fa icon="lock"/></template>
</ui-input>
<ui-input v-if="user && user.twoFactorEnabled" v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false" required>
<span>{{ $t('@.2fa') }}</span>
<template #prefix><fa icon="gavel"/></template>
</ui-input>
<ui-button type="submit" :disabled="signing">{{ signing ? $t('signing-in') : $t('@.signin') }}</ui-button>
<p v-if="meta && meta.enableTwitterIntegration" style="margin: 8px 0;"><a :href="`${apiUrl}/signin/twitter`"><fa :icon="['fab', 'twitter']"/> {{ $t('signin-with-twitter') }}</a></p>
<p v-if="meta && meta.enableGithubIntegration" style="margin: 8px 0;"><a :href="`${apiUrl}/signin/github`"><fa :icon="['fab', 'github']"/> {{ $t('signin-with-github') }}</a></p>
<p v-if="meta && meta.enableDiscordIntegration" style="margin: 8px 0;"><a :href="`${apiUrl}/signin/discord`"><fa :icon="['fab', 'discord']"/> {{ $t('signin-with-discord') /* TODO: Make these layouts better */ }}</a></p>
<div class="normal-signin" v-if="!totpLogin">
<ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required @input="onUsernameChange">
<span>{{ $t('username') }}</span>
<template #prefix>@</template>
<template #suffix>@{{ host }}</template>
</ui-input>
<ui-input v-model="password" type="password" :with-password-toggle="true" required>
<span>{{ $t('password') }}</span>
<template #prefix><fa icon="lock"/></template>
</ui-input>
<ui-button type="submit" :disabled="signing">{{ signing ? $t('signing-in') : $t('@.signin') }}</ui-button>
<p v-if="meta && meta.enableTwitterIntegration" style="margin: 8px 0;"><a :href="`${apiUrl}/signin/twitter`"><fa :icon="['fab', 'twitter']"/> {{ $t('signin-with-twitter') }}</a></p>
<p v-if="meta && meta.enableGithubIntegration" style="margin: 8px 0;"><a :href="`${apiUrl}/signin/github`"><fa :icon="['fab', 'github']"/> {{ $t('signin-with-github') }}</a></p>
<p v-if="meta && meta.enableDiscordIntegration" style="margin: 8px 0;"><a :href="`${apiUrl}/signin/discord`"><fa :icon="['fab', 'discord']"/> {{ $t('signin-with-discord') /* TODO: Make these layouts better */ }}</a></p>
</div>
<div class="2fa-signin" v-if="totpLogin" :class="{ securityKeys: user && user.securityKeys }">
<div v-if="user && user.securityKeys" class="twofa-group tap-group">
<p>{{ $t('tap-key') }}</p>
<ui-button @click="queryKey" v-if="!queryingKey">
{{ $t('@.error.retry') }}
</ui-button>
</div>
<div class="or-hr" v-if="user && user.securityKeys">
<p class="or-msg">{{ $t('or') }}</p>
</div>
<div class="twofa-group totp-group">
<p style="margin-bottom:0;">{{ $t('enter-2fa-code') }}</p>
<ui-input v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false" required>
<span>{{ $t('@.2fa') }}</span>
<template #prefix><fa icon="gavel"/></template>
</ui-input>
<ui-button type="submit" :disabled="signing">{{ signing ? $t('signing-in') : $t('@.signin') }}</ui-button>
</div>
</div>
</form>
</template>
@ -26,6 +43,7 @@ import Vue from 'vue';
import i18n from '../../../i18n';
import { apiUrl, host } from '../../../config';
import { toUnicode } from 'punycode';
import { hexifyAB } from '../../scripts/2fa';
export default Vue.extend({
i18n: i18n('common/views/components/signin.vue'),
@ -47,7 +65,11 @@ export default Vue.extend({
token: '',
apiUrl,
host: toUnicode(host),
meta: null
meta: null,
totpLogin: false,
credential: null,
challengeData: null,
queryingKey: false,
};
},
@ -68,23 +90,87 @@ export default Vue.extend({
});
},
onSubmit() {
this.signing = true;
this.$root.api('signin', {
username: this.username,
password: this.password,
token: this.user && this.user.twoFactorEnabled ? this.token : undefined
queryKey() {
this.queryingKey = true;
return navigator.credentials.get({
publicKey: {
challenge: Buffer.from(
this.challengeData.challenge
.replace(/\-/g, '+')
.replace(/_/g, '/'),
'base64'
),
allowCredentials: this.challengeData.securityKeys.map(key => ({
id: Buffer.from(key.id, 'hex'),
type: 'public-key',
transports: ['usb', 'ble', 'nfc']
})),
timeout: 60 * 1000
}
}).catch(err => {
this.queryingKey = false;
console.warn(err);
return Promise.reject(null);
}).then(credential => {
this.queryingKey = false;
this.signing = true;
return this.$root.api('signin', {
username: this.username,
password: this.password,
signature: hexifyAB(credential.response.signature),
authenticatorData: hexifyAB(credential.response.authenticatorData),
clientDataJSON: hexifyAB(credential.response.clientDataJSON),
credentialId: credential.id,
challengeId: this.challengeData.challengeId
});
}).then(res => {
localStorage.setItem('i', res.i);
location.reload();
}).catch(() => {
}).catch(err => {
if(err === null) return;
console.error(err);
this.$root.dialog({
type: 'error',
text: this.$t('login-failed')
});
this.signing = false;
});
},
onSubmit() {
this.signing = true;
if (!this.totpLogin && this.user && this.user.twoFactorEnabled) {
if (window.PublicKeyCredential && this.user.securityKeys) {
this.$root.api('i/2fa/getkeys', {
username: this.username,
password: this.password
}).then(res => {
this.totpLogin = true;
this.signing = false;
this.challengeData = res;
return this.queryKey();
});
} else {
this.totpLogin = true;
this.signing = false;
}
} else {
this.$root.api('signin', {
username: this.username,
password: this.password,
token: this.user && this.user.twoFactorEnabled ? this.token : undefined
}).then(res => {
localStorage.setItem('i', res.i);
location.reload();
}).catch(() => {
this.$root.dialog({
type: 'error',
text: this.$t('login-failed')
});
this.signing = false;
});
}
}
}
});
@ -94,6 +180,48 @@ export default Vue.extend({
.mk-signin
color #555
.or-hr,
.or-hr .or-msg,
.twofa-group,
.twofa-group p
color var(--text)
.tap-group > button
margin-bottom 1em
.securityKeys .or-hr
&
position relative
.or-msg
&:before
right 100%
margin-right 0.125em
&:after
left 100%
margin-left 0.125em
&:before, &:after
content ""
position absolute
top 50%
width 100%
height 2px
background #555
&
position relative
margin auto
left 0
right 0
top 0
bottom 0
font-size 1.5em
height 1.5em
width 3em
text-align center
&.signing
&, *
cursor wait !important

View file

@ -5,6 +5,9 @@
<span class="hostname">{{ hostname }}</span>
<span class="port" v-if="port != ''">:{{ port }}</span>
</template>
<template v-if="pathname === '/' && self">
<span class="self">{{ hostname }}</span>
</template>
<span class="pathname" v-if="pathname != ''">{{ self ? pathname.substr(1) : pathname }}</span>
<span class="query">{{ query }}</span>
<span class="hash">{{ hash }}</span>
@ -22,6 +25,7 @@ export default Vue.extend({
data() {
const isSelf = this.url.startsWith(local);
const hasRoute = isSelf && (
(this.url.substr(local.length) === '/') ||
this.url.substr(local.length).startsWith('/@') ||
this.url.substr(local.length).startsWith('/notes/') ||
this.url.substr(local.length).startsWith('/pages/'));
@ -54,19 +58,28 @@ export default Vue.extend({
<style lang="stylus" scoped>
.mk-url
word-break break-all
> [data-icon]
padding-left 2px
font-size .9em
font-weight 400
font-style normal
> .self
font-weight bold
> .schema
opacity 0.5
> .hostname
font-weight bold
> .pathname
opacity 0.8
> .query
opacity 0.5
> .hash
font-style italic
</style>

View file

@ -97,7 +97,9 @@ export default Vue.extend({
const image = [
'image/jpeg',
'image/png',
'image/gif'
'image/gif',
'image/apng',
'image/vnd.mozilla.apng',
];
this.$root.api('users/notes', {

View file

@ -91,6 +91,7 @@ export default ($root: any) => {
? Promise.resolve(file)
: $root.$chooseDriveFile({
multiple: false,
type: 'image/*',
title: locale['desktop']['choose-avatar']
});

View file

@ -91,6 +91,7 @@ export default ($root: any) => {
? Promise.resolve(file)
: $root.$chooseDriveFile({
multiple: false,
type: 'image/*',
title: locale['desktop']['choose-banner']
});

View file

@ -77,6 +77,7 @@ init(async (launch, os) => {
if (document.body.clientWidth > 800) {
const w = this.$root.new(MkChooseFileFromDriveWindow, {
title: o.title,
type: o.type,
multiple: o.multiple,
initFolder: o.currentFolder
});

View file

@ -11,6 +11,7 @@
<x-drive
ref="browser"
class="browser"
:type="type"
:multiple="multiple"
@selected="onSelected"
@change-selection="onChangeSelection"
@ -33,6 +34,11 @@ export default Vue.extend({
XDrive: () => import('./drive.vue').then(m => m.default),
},
props: {
type: {
type: String,
required: false,
default: undefined
},
multiple: {
default: false
}

View file

@ -21,6 +21,7 @@
import Vue from 'vue';
import i18n from '../../../i18n';
import VueCropper from 'vue-cropperjs';
import 'cropperjs/dist/cropper.css';
import * as url from '../../../../../prelude/url';
export default Vue.extend({

View file

@ -80,6 +80,11 @@ export default Vue.extend({
type: Object,
required: false
},
type: {
type: String,
required: false,
default: undefined
},
multiple: {
type: Boolean,
default: false
@ -540,6 +545,7 @@ export default Vue.extend({
//
this.$root.api('drive/files', {
folderId: this.folder ? this.folder.id : null,
type: this.type,
limit: filesMax + 1
}).then(files => {
if (files.length == filesMax + 1) {
@ -570,6 +576,7 @@ export default Vue.extend({
//
this.$root.api('drive/files', {
folderId: this.folder ? this.folder.id : null,
type: this.type,
untilId: this.files[this.files.length - 1].id,
limit: max + 1
}).then(files => {

View file

@ -38,7 +38,9 @@ export default Vue.extend({
const image = [
'image/jpeg',
'image/png',
'image/gif'
'image/gif',
'image/apng',
'image/vnd.mozilla.apng',
];
this.$root.api('users/notes', {

View file

@ -186,7 +186,9 @@ export default Vue.extend({
const image = [
'image/jpeg',
'image/png',
'image/gif'
'image/gif',
'image/apng',
'image/vnd.mozilla.apng',
];
this.$root.api('notes/local-timeline', {

View file

@ -8,6 +8,7 @@
</header>
<x-drive class="drive" ref="browser"
:select-file="true"
:type="type"
:multiple="multiple"
@change-selection="onChangeSelection"
@selected="onSelected"
@ -25,7 +26,7 @@ export default Vue.extend({
components: {
XDrive: () => import('./drive.vue').then(m => m.default),
},
props: ['multiple'],
props: ['type', 'multiple'],
data() {
return {
files: []

View file

@ -30,7 +30,9 @@ export default Vue.extend({
const image = [
'image/jpeg',
'image/png',
'image/gif'
'image/gif',
'image/apng',
'image/vnd.mozilla.apng',
];
this.$root.api('users/notes', {
userId: this.user.id,

View file

@ -110,7 +110,9 @@ export default Vue.extend({
const image = [
'image/jpeg',
'image/png',
'image/gif'
'image/gif',
'image/apng',
'image/vnd.mozilla.apng',
];
this.$root.api('notes/local-timeline', {

View file

@ -1,25 +0,0 @@
import VueRouter from 'vue-router';
// Style
import './style.styl';
import init from '../init';
import Index from './views/index.vue';
import NotFound from '../common/views/pages/not-found.vue';
init(launch => {
document.title = 'Misskey';
// Init router
const router = new VueRouter({
mode: 'history',
base: '/test/',
routes: [
{ path: '/', component: Index },
{ path: '*', component: NotFound }
]
});
// Launch the app
launch(router);
});

View file

@ -1,6 +0,0 @@
@import "../app"
@import "../reset"
html
height 100%
background var(--bg)

View file

@ -1,82 +0,0 @@
<template>
<main>
<ui-card>
<template #title>MFM Playground</template>
<section class="fit-top">
<ui-textarea v-model="mfm">
<span>MFM</span>
</ui-textarea>
</section>
<section>
<header>Preview</header>
<mfm :text="mfm" :i="$store.state.i"/>
</section>
<section>
<header style="margin-bottom:0;">AST</header>
<ui-textarea v-model="mfmAst" readonly tall style="margin-top:16px;"></ui-textarea>
</section>
</ui-card>
<ui-card>
<template #title>Dialog Generator</template>
<section class="fit-top">
<ui-select v-model="dialogType" placeholder="">
<option value="info">Information</option>
<option value="success">Success</option>
<option value="warning">Warning</option>
<option value="error">Error</option>
</ui-select>
<ui-input v-model="dialogTitle">
<span>Title</span>
</ui-input>
<ui-input v-model="dialogText">
<span>Text</span>
</ui-input>
<ui-switch v-model="dialogShowCancelButton">With cancel button</ui-switch>
<ui-button @click="showDialog">Show</ui-button>
</section>
</ui-card>
</main>
</template>
<script lang="ts">
import Vue from 'vue';
import { parse } from '../../../../mfm/parse';
import * as JSON5 from 'json5';
export default Vue.extend({
data() {
return {
mfm: '',
dialogType: 'success',
dialogTitle: '',
dialogText: 'Hello World!',
dialogShowCancelButton: false
};
},
computed: {
mfmAst(): any {
return JSON5.stringify(parse(this.mfm), null, 2);
}
},
methods: {
showDialog() {
this.$root.dialog({
type: this.dialogType,
title: this.dialogTitle,
text: this.dialogText,
showCancelButton: this.dialogShowCancelButton
});
}
}
});
</script>
<style lang="stylus" scoped>
main
max-width 700px
margin 0 auto
</style>

18
src/daemons/janitor.ts Normal file
View file

@ -0,0 +1,18 @@
const interval = 30 * 60 * 1000;
import { AttestationChallenges } from '../models';
import { LessThan } from 'typeorm';
/**
* Clean up database occasionally
*/
export default function() {
async function tick() {
await AttestationChallenges.delete({
createdAt: LessThan(new Date(new Date().getTime() - 5 * 60 * 1000))
});
}
tick();
setInterval(tick, interval);
}

View file

@ -43,6 +43,8 @@ import { Poll } from '../models/entities/poll';
import { UserKeypair } from '../models/entities/user-keypair';
import { UserPublickey } from '../models/entities/user-publickey';
import { UserProfile } from '../models/entities/user-profile';
import { UserSecurityKey } from '../models/entities/user-security-key';
import { AttestationChallenge } from '../models/entities/attestation-challenge';
import { Page } from '../models/entities/page';
import { PageLike } from '../models/entities/page-like';
@ -80,6 +82,53 @@ class MyCustomLogger implements Logger {
}
}
export const entities = [
Meta,
Instance,
App,
AuthSession,
AccessToken,
User,
UserProfile,
UserKeypair,
UserPublickey,
UserList,
UserListJoining,
UserGroup,
UserGroupJoining,
UserGroupInvite,
UserNotePining,
UserSecurityKey,
AttestationChallenge,
Following,
FollowRequest,
Muting,
Blocking,
Note,
NoteFavorite,
NoteReaction,
NoteWatching,
NoteUnread,
Page,
PageLike,
Log,
DriveFile,
DriveFolder,
Poll,
PollVote,
Notification,
Emoji,
Hashtag,
SwSubscription,
AbuseUserReport,
RegistrationTicket,
MessagingMessage,
Signin,
ReversiGame,
ReversiMatching,
...charts as any
];
export function initDb(justBorrow = false, sync = false, log = false) {
try {
const conn = getConnection();
@ -101,7 +150,7 @@ export function initDb(justBorrow = false, sync = false, log = false) {
options: {
host: config.redis.host,
port: config.redis.port,
options:{
options: {
password: config.redis.pass,
prefix: config.redis.prefix,
db: config.redis.db || 0
@ -110,49 +159,6 @@ export function initDb(justBorrow = false, sync = false, log = false) {
} : false,
logging: log,
logger: log ? new MyCustomLogger() : undefined,
entities: [
Meta,
Instance,
App,
AuthSession,
AccessToken,
User,
UserProfile,
UserKeypair,
UserPublickey,
UserList,
UserListJoining,
UserGroup,
UserGroupJoining,
UserGroupInvite,
UserNotePining,
Following,
FollowRequest,
Muting,
Blocking,
Note,
NoteFavorite,
NoteReaction,
NoteWatching,
NoteUnread,
Page,
PageLike,
Log,
DriveFile,
DriveFolder,
Poll,
PollVote,
Notification,
Emoji,
Hashtag,
SwSubscription,
AbuseUserReport,
RegistrationTicket,
MessagingMessage,
Signin,
ReversiGame,
ReversiMatching,
...charts as any
]
entities: entities
});
}

View file

@ -6,8 +6,8 @@ import { toUnicode } from 'punycode';
import { emojiRegex } from '../misc/emoji-regex';
export function removeOrphanedBrackets(s: string): string {
const openBrackets = ['(', '「'];
const closeBrackets = [')', '」'];
const openBrackets = ['(', '「', '['];
const closeBrackets = [')', '」', ']'];
const xs = cumulativeSum(s.split('').map(c => {
if (openBrackets.includes(c)) return 1;
if (closeBrackets.includes(c)) return -1;

View file

@ -1,5 +1,5 @@
import * as fs from 'fs';
import fileType from 'file-type';
import fileType = require('file-type');
import checkSvg from '../misc/check-svg';
export async function detectMine(path: string) {

View file

@ -5,7 +5,7 @@ import chalk from 'chalk';
import Logger from '../services/logger';
export async function downloadUrl(url: string, path: string) {
const logger = new Logger('download-url');
const logger = new Logger('download');
await new Promise((res, rej) => {
logger.info(`Downloading ${chalk.cyan(url)} ...`);

View file

@ -1,17 +1,3 @@
export const types = {
boolean: 'boolean' as 'boolean',
string: 'string' as 'string',
number: 'number' as 'number',
array: 'array' as 'array',
object: 'object' as 'object',
any: 'any' as 'any',
};
export const bool = {
true: true as true,
false: false as false,
};
export type Schema = {
type: 'boolean' | 'number' | 'string' | 'array' | 'object' | 'any';
nullable: boolean;

View file

@ -0,0 +1,46 @@
import { PrimaryColumn, Entity, JoinColumn, Column, ManyToOne, Index } from 'typeorm';
import { User } from './user';
import { id } from '../id';
@Entity()
export class AttestationChallenge {
@PrimaryColumn(id())
public id: string;
@Index()
@PrimaryColumn(id())
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Index()
@Column('varchar', {
length: 64,
comment: 'Hex-encoded sha256 hash of the challenge.'
})
public challenge: string;
@Column('timestamp with time zone', {
comment: 'The date challenge was created for expiry purposes.'
})
public createdAt: Date;
@Column('boolean', {
comment:
'Indicates that the challenge is only for registration purposes if true to prevent the challenge for being used as authentication.',
default: false
})
public registrationChallenge: boolean;
constructor(data: Partial<AttestationChallenge>) {
if (data == null) return;
for (const [k, v] of Object.entries(data)) {
(this as any)[k] = v;
}
}
}

View file

@ -76,6 +76,11 @@ export class UserProfile {
})
public twoFactorEnabled: boolean;
@Column('boolean', {
default: false,
})
public securityKeysAvailable: boolean;
@Column('varchar', {
length: 128, nullable: true,
comment: 'The password hash of the User. It will be null if the origin of the user is local.'

View file

@ -0,0 +1,48 @@
import { PrimaryColumn, Entity, JoinColumn, Column, ManyToOne, Index } from 'typeorm';
import { User } from './user';
import { id } from '../id';
@Entity()
export class UserSecurityKey {
@PrimaryColumn('varchar', {
comment: 'Variable-length id given to navigator.credentials.get()'
})
public id: string;
@Index()
@Column(id())
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Index()
@Column('varchar', {
comment:
'Variable-length public key used to verify attestations (hex-encoded).'
})
public publicKey: string;
@Column('timestamp with time zone', {
comment:
'The date of the last time the UserSecurityKey was successfully validated.'
})
public lastUsed: Date;
@Column('varchar', {
comment: 'User-defined name for this key',
length: 30
})
public name: string;
constructor(data: Partial<UserSecurityKey>) {
if (data == null) return;
for (const [k, v] of Object.entries(data)) {
(this as any)[k] = v;
}
}
}

View file

@ -1,4 +1,4 @@
export const id = () => ({
type: 'varchar' as 'varchar',
type: 'varchar' as const,
length: 32
});

View file

@ -37,6 +37,8 @@ import { FollowingRepository } from './repositories/following';
import { AbuseUserReportRepository } from './repositories/abuse-user-report';
import { AuthSessionRepository } from './repositories/auth-session';
import { UserProfile } from './entities/user-profile';
import { AttestationChallenge } from './entities/attestation-challenge';
import { UserSecurityKey } from './entities/user-security-key';
import { HashtagRepository } from './repositories/hashtag';
import { PageRepository } from './repositories/page';
import { PageLikeRepository } from './repositories/page-like';
@ -52,6 +54,8 @@ export const PollVotes = getRepository(PollVote);
export const Users = getCustomRepository(UserRepository);
export const UserProfiles = getRepository(UserProfile);
export const UserKeypairs = getRepository(UserKeypair);
export const AttestationChallenges = getRepository(AttestationChallenge);
export const UserSecurityKeys = getRepository(UserSecurityKey);
export const UserPublickeys = getRepository(UserPublickey);
export const UserLists = getCustomRepository(UserListRepository);
export const UserListJoinings = getRepository(UserListJoining);

View file

@ -2,7 +2,7 @@ import { EntityRepository, Repository } from 'typeorm';
import { App } from '../entities/app';
import { AccessTokens } from '..';
import { ensure } from '../../prelude/ensure';
import { types, bool, SchemaType } from '../../misc/schema';
import { SchemaType } from '../../misc/schema';
export type PackedApp = SchemaType<typeof packedAppSchema>;
@ -42,37 +42,37 @@ export class AppRepository extends Repository<App> {
}
export const packedAppSchema = {
type: types.object,
optional: bool.false, nullable: bool.false,
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
description: 'The unique identifier for this Note.',
example: 'xxxxxxxxxx',
},
name: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
description: 'アプリケーションの名前'
},
callbackUrl: {
type: types.string,
optional: bool.false, nullable: bool.true,
type: 'string' as const,
optional: false as const, nullable: true as const,
description: 'コールバックするURL'
},
permission: {
type: types.array,
optional: bool.true, nullable: bool.false,
type: 'array' as const,
optional: true as const, nullable: false as const,
items: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
}
},
secret: {
type: types.string,
optional: bool.true, nullable: bool.false,
type: 'string' as const,
optional: true as const, nullable: false as const,
description: 'アプリケーションのシークレットキー'
}
},

View file

@ -3,7 +3,7 @@ import { Users } from '..';
import { Blocking } from '../entities/blocking';
import { ensure } from '../../prelude/ensure';
import { awaitAll } from '../../prelude/await-all';
import { SchemaType, types, bool } from '../../misc/schema';
import { SchemaType } from '../../misc/schema';
export type PackedBlocking = SchemaType<typeof packedBlockingSchema>;
@ -34,30 +34,30 @@ export class BlockingRepository extends Repository<Blocking> {
}
export const packedBlockingSchema = {
type: types.object,
optional: bool.false, nullable: bool.false,
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
description: 'The unique identifier for this blocking.',
example: 'xxxxxxxxxx',
},
createdAt: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'date-time',
description: 'The date that the blocking was created.'
},
blockeeId: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
},
blockee: {
type: types.object,
optional: bool.false, nullable: bool.false,
type: 'object' as const,
optional: false as const, nullable: false as const,
ref: 'User',
description: 'The blockee.'
},

View file

@ -5,7 +5,7 @@ import { User } from '../entities/user';
import { toPuny } from '../../misc/convert-host';
import { ensure } from '../../prelude/ensure';
import { awaitAll } from '../../prelude/await-all';
import { types, bool, SchemaType } from '../../misc/schema';
import { SchemaType } from '../../misc/schema';
export type PackedDriveFile = SchemaType<typeof packedDriveFileSchema>;
@ -114,63 +114,63 @@ export class DriveFileRepository extends Repository<DriveFile> {
}
export const packedDriveFileSchema = {
type: types.object,
optional: bool.false, nullable: bool.false,
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
description: 'The unique identifier for this Drive file.',
example: 'xxxxxxxxxx',
},
createdAt: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'date-time',
description: 'The date that the Drive file was created on Misskey.'
},
name: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
description: 'The file name with extension.',
example: 'lenna.jpg'
},
type: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
description: 'The MIME type of this Drive file.',
example: 'image/jpeg'
},
md5: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'md5',
description: 'The MD5 hash of this Drive file.',
example: '15eca7fba0480996e2245f5185bf39f2'
},
size: {
type: types.number,
optional: bool.false, nullable: bool.false,
type: 'number' as const,
optional: false as const, nullable: false as const,
description: 'The size of this Drive file. (bytes)',
example: 51469
},
url: {
type: types.string,
optional: bool.false, nullable: bool.true,
type: 'string' as const,
optional: false as const, nullable: true as const,
format: 'url',
description: 'The URL of this Drive file.',
},
folderId: {
type: types.string,
optional: bool.false, nullable: bool.true,
type: 'string' as const,
optional: false as const, nullable: true as const,
format: 'id',
description: 'The parent folder ID of this Drive file.',
example: 'xxxxxxxxxx',
},
isSensitive: {
type: types.boolean,
optional: bool.false, nullable: bool.false,
type: 'boolean' as const,
optional: false as const, nullable: false as const,
description: 'Whether this Drive file is sensitive.',
},
},

View file

@ -3,7 +3,7 @@ import { DriveFolders, DriveFiles } from '..';
import { DriveFolder } from '../entities/drive-folder';
import { ensure } from '../../prelude/ensure';
import { awaitAll } from '../../prelude/await-all';
import { SchemaType, types, bool } from '../../misc/schema';
import { SchemaType } from '../../misc/schema';
export type PackedDriveFolder = SchemaType<typeof packedDriveFolderSchema>;
@ -53,47 +53,47 @@ export class DriveFolderRepository extends Repository<DriveFolder> {
}
export const packedDriveFolderSchema = {
type: types.object,
optional: bool.false, nullable: bool.false,
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
description: 'The unique identifier for this Drive folder.',
example: 'xxxxxxxxxx',
},
createdAt: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'date-time',
description: 'The date that the Drive folder was created.'
},
name: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
description: 'The folder name.',
},
foldersCount: {
type: types.number,
optional: bool.true, nullable: bool.false,
type: 'number' as const,
optional: true as const, nullable: false as const,
description: 'The count of child folders.',
},
filesCount: {
type: types.number,
optional: bool.true, nullable: bool.false,
type: 'number' as const,
optional: true as const, nullable: false as const,
description: 'The count of child files.',
},
parentId: {
type: types.string,
optional: bool.false, nullable: bool.true,
type: 'string' as const,
optional: false as const, nullable: true as const,
format: 'id',
description: 'The parent folder ID of this folder.',
example: 'xxxxxxxxxx',
},
parent: {
type: types.object,
optional: bool.true, nullable: bool.true,
type: 'object' as const,
optional: true as const, nullable: true as const,
ref: 'DriveFolder'
},
},

View file

@ -3,7 +3,7 @@ import { Users } from '..';
import { Following } from '../entities/following';
import { ensure } from '../../prelude/ensure';
import { awaitAll } from '../../prelude/await-all';
import { SchemaType, types, bool } from '../../misc/schema';
import { SchemaType } from '../../misc/schema';
type LocalFollowerFollowing = Following & {
followerHost: null;
@ -88,41 +88,41 @@ export class FollowingRepository extends Repository<Following> {
}
export const packedFollowingSchema = {
type: types.object,
optional: bool.false, nullable: bool.false,
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
description: 'The unique identifier for this following.',
example: 'xxxxxxxxxx',
},
createdAt: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'date-time',
description: 'The date that the following was created.'
},
followeeId: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
},
followee: {
type: types.object,
optional: bool.true, nullable: bool.false,
type: 'object' as const,
optional: true as const, nullable: false as const,
ref: 'User',
description: 'The followee.'
},
followerId: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
},
follower: {
type: types.object,
optional: bool.true, nullable: bool.false,
type: 'object' as const,
optional: true as const, nullable: false as const,
ref: 'User',
description: 'The follower.'
},

View file

@ -1,6 +1,6 @@
import { EntityRepository, Repository } from 'typeorm';
import { Hashtag } from '../entities/hashtag';
import { SchemaType, types, bool } from '../../misc/schema';
import { SchemaType } from '../../misc/schema';
export type PackedHashtag = SchemaType<typeof packedHashtagSchema>;
@ -28,43 +28,43 @@ export class HashtagRepository extends Repository<Hashtag> {
}
export const packedHashtagSchema = {
type: types.object,
optional: bool.false, nullable: bool.false,
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
tag: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
description: 'The hashtag name. No # prefixed.',
example: 'misskey',
},
mentionedUsersCount: {
type: types.number,
optional: bool.false, nullable: bool.false,
type: 'number' as const,
optional: false as const, nullable: false as const,
description: 'Number of all users using this hashtag.'
},
mentionedLocalUsersCount: {
type: types.number,
optional: bool.false, nullable: bool.false,
type: 'number' as const,
optional: false as const, nullable: false as const,
description: 'Number of local users using this hashtag.'
},
mentionedRemoteUsersCount: {
type: types.number,
optional: bool.false, nullable: bool.false,
type: 'number' as const,
optional: false as const, nullable: false as const,
description: 'Number of remote users using this hashtag.'
},
attachedUsersCount: {
type: types.number,
optional: bool.false, nullable: bool.false,
type: 'number' as const,
optional: false as const, nullable: false as const,
description: 'Number of all users who attached this hashtag to profile.'
},
attachedLocalUsersCount: {
type: types.number,
optional: bool.false, nullable: bool.false,
type: 'number' as const,
optional: false as const, nullable: false as const,
description: 'Number of local users who attached this hashtag to profile.'
},
attachedRemoteUsersCount: {
type: types.number,
optional: bool.false, nullable: bool.false,
type: 'number' as const,
optional: false as const, nullable: false as const,
description: 'Number of remote users who attached this hashtag to profile.'
},
}

View file

@ -2,7 +2,7 @@ import { EntityRepository, Repository } from 'typeorm';
import { MessagingMessage } from '../entities/messaging-message';
import { Users, DriveFiles, UserGroups } from '..';
import { ensure } from '../../prelude/ensure';
import { types, bool, SchemaType } from '../../misc/schema';
import { SchemaType } from '../../misc/schema';
export type PackedMessagingMessage = SchemaType<typeof packedMessagingMessageSchema>;
@ -46,76 +46,76 @@ export class MessagingMessageRepository extends Repository<MessagingMessage> {
}
export const packedMessagingMessageSchema = {
type: types.object,
optional: bool.false, nullable: bool.false,
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
description: 'The unique identifier for this MessagingMessage.',
example: 'xxxxxxxxxx',
},
createdAt: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'date-time',
description: 'The date that the MessagingMessage was created.'
},
userId: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
},
user: {
type: types.object,
type: 'object' as const,
ref: 'User',
optional: bool.true, nullable: bool.false,
optional: true as const, nullable: false as const,
},
text: {
type: types.string,
optional: bool.false, nullable: bool.true,
type: 'string' as const,
optional: false as const, nullable: true as const,
},
fileId: {
type: types.string,
optional: bool.true, nullable: bool.true,
type: 'string' as const,
optional: true as const, nullable: true as const,
format: 'id',
},
file: {
type: types.object,
optional: bool.true, nullable: bool.true,
type: 'object' as const,
optional: true as const, nullable: true as const,
ref: 'DriveFile',
},
recipientId: {
type: types.string,
optional: bool.false, nullable: bool.true,
type: 'string' as const,
optional: false as const, nullable: true as const,
format: 'id',
},
recipient: {
type: types.object,
optional: bool.true, nullable: bool.true,
type: 'object' as const,
optional: true as const, nullable: true as const,
ref: 'User'
},
groupId: {
type: types.string,
optional: bool.false, nullable: bool.true,
type: 'string' as const,
optional: false as const, nullable: true as const,
format: 'id',
},
group: {
type: types.object,
optional: bool.true, nullable: bool.true,
type: 'object' as const,
optional: true as const, nullable: true as const,
ref: 'UserGroup'
},
isRead: {
type: types.boolean,
optional: bool.true, nullable: bool.false,
type: 'boolean' as const,
optional: true as const, nullable: false as const,
},
reads: {
type: types.array,
optional: bool.true, nullable: bool.false,
type: 'array' as const,
optional: true as const, nullable: false as const,
items: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id'
}
},

View file

@ -3,7 +3,7 @@ import { Users } from '..';
import { Muting } from '../entities/muting';
import { ensure } from '../../prelude/ensure';
import { awaitAll } from '../../prelude/await-all';
import { types, bool, SchemaType } from '../../misc/schema';
import { SchemaType } from '../../misc/schema';
export type PackedMuting = SchemaType<typeof packedMutingSchema>;
@ -34,30 +34,30 @@ export class MutingRepository extends Repository<Muting> {
}
export const packedMutingSchema = {
type: types.object,
optional: bool.false, nullable: bool.false,
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
description: 'The unique identifier for this muting.',
example: 'xxxxxxxxxx',
},
createdAt: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'date-time',
description: 'The date that the muting was created.'
},
muteeId: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
},
mutee: {
type: types.object,
optional: bool.false, nullable: bool.false,
type: 'object' as const,
optional: false as const, nullable: false as const,
ref: 'User',
description: 'The mutee.'
},

View file

@ -2,7 +2,6 @@ import { EntityRepository, Repository } from 'typeorm';
import { NoteFavorite } from '../entities/note-favorite';
import { Notes } from '..';
import { ensure } from '../../prelude/ensure';
import { types, bool } from '../../misc/schema';
@EntityRepository(NoteFavorite)
export class NoteFavoriteRepository extends Repository<NoteFavorite> {
@ -29,30 +28,30 @@ export class NoteFavoriteRepository extends Repository<NoteFavorite> {
}
export const packedNoteFavoriteSchema = {
type: types.object,
optional: bool.false, nullable: bool.false,
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
description: 'The unique identifier for this favorite.',
example: 'xxxxxxxxxx',
},
createdAt: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'date-time',
description: 'The date that the favorite was created.'
},
note: {
type: types.object,
optional: bool.false, nullable: bool.false,
type: 'object' as const,
optional: false as const, nullable: false as const,
ref: 'Note',
},
noteId: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
},
},

View file

@ -2,7 +2,7 @@ import { EntityRepository, Repository } from 'typeorm';
import { NoteReaction } from '../entities/note-reaction';
import { Users } from '..';
import { ensure } from '../../prelude/ensure';
import { types, bool, SchemaType } from '../../misc/schema';
import { SchemaType } from '../../misc/schema';
export type PackedNoteReaction = SchemaType<typeof packedNoteReactionSchema>;
@ -24,31 +24,31 @@ export class NoteReactionRepository extends Repository<NoteReaction> {
}
export const packedNoteReactionSchema = {
type: types.object,
optional: bool.false, nullable: bool.false,
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
description: 'The unique identifier for this reaction.',
example: 'xxxxxxxxxx',
},
createdAt: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'date-time',
description: 'The date that the reaction was created.'
},
user: {
type: types.object,
optional: bool.false, nullable: bool.false,
type: 'object' as const,
optional: false as const, nullable: false as const,
ref: 'User',
description: 'User who performed this reaction.'
},
type: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
description: 'The reaction type.'
},
},

View file

@ -5,7 +5,7 @@ import { unique, concat } from '../../prelude/array';
import { nyaize } from '../../misc/nyaize';
import { Emojis, Users, Apps, PollVotes, DriveFiles, NoteReactions, Followings, Polls } from '..';
import { ensure } from '../../prelude/ensure';
import { SchemaType, types, bool } from '../../misc/schema';
import { SchemaType } from '../../misc/schema';
import { awaitAll } from '../../prelude/await-all';
export type PackedNote = SchemaType<typeof packedNoteSchema>;
@ -144,8 +144,8 @@ export class NoteRepository extends Repository<Note> {
let text = note.text;
if (note.name) {
text = `${note.name}\n${note.text}`;
if (note.name && note.uri) {
text = `${note.name}\n${(note.text || '').trim()}\n${note.uri}`;
}
const reactionEmojis = unique(concat([note.emojis, Object.keys(note.reactions)]));
@ -218,125 +218,125 @@ export class NoteRepository extends Repository<Note> {
}
export const packedNoteSchema = {
type: types.object,
optional: bool.false, nullable: bool.false,
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
description: 'The unique identifier for this Note.',
example: 'xxxxxxxxxx',
},
createdAt: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'date-time',
description: 'The date that the Note was created on Misskey.'
},
text: {
type: types.string,
optional: bool.false, nullable: bool.true,
type: 'string' as const,
optional: false as const, nullable: true as const,
},
cw: {
type: types.string,
optional: bool.true, nullable: bool.true,
type: 'string' as const,
optional: true as const, nullable: true as const,
},
userId: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
},
user: {
type: types.object,
type: 'object' as const,
ref: 'User',
optional: bool.false, nullable: bool.false,
optional: false as const, nullable: false as const,
},
replyId: {
type: types.string,
optional: bool.true, nullable: bool.true,
type: 'string' as const,
optional: true as const, nullable: true as const,
format: 'id',
example: 'xxxxxxxxxx',
},
renoteId: {
type: types.string,
optional: bool.true, nullable: bool.true,
type: 'string' as const,
optional: true as const, nullable: true as const,
format: 'id',
example: 'xxxxxxxxxx',
},
reply: {
type: types.object,
optional: bool.true, nullable: bool.true,
type: 'object' as const,
optional: true as const, nullable: true as const,
ref: 'Note'
},
renote: {
type: types.object,
optional: bool.true, nullable: bool.true,
type: 'object' as const,
optional: true as const, nullable: true as const,
ref: 'Note'
},
viaMobile: {
type: types.boolean,
optional: bool.true, nullable: bool.false,
type: 'boolean' as const,
optional: true as const, nullable: false as const,
},
isHidden: {
type: types.boolean,
optional: bool.true, nullable: bool.false,
type: 'boolean' as const,
optional: true as const, nullable: false as const,
},
visibility: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
},
mentions: {
type: types.array,
optional: bool.true, nullable: bool.false,
type: 'array' as const,
optional: true as const, nullable: false as const,
items: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id'
}
},
visibleUserIds: {
type: types.array,
optional: bool.true, nullable: bool.false,
type: 'array' as const,
optional: true as const, nullable: false as const,
items: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id'
}
},
fileIds: {
type: types.array,
optional: bool.true, nullable: bool.false,
type: 'array' as const,
optional: true as const, nullable: false as const,
items: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id'
}
},
files: {
type: types.array,
optional: bool.true, nullable: bool.false,
type: 'array' as const,
optional: true as const, nullable: false as const,
items: {
type: types.object,
optional: bool.false, nullable: bool.false,
type: 'object' as const,
optional: false as const, nullable: false as const,
ref: 'DriveFile'
}
},
tags: {
type: types.array,
optional: bool.true, nullable: bool.false,
type: 'array' as const,
optional: true as const, nullable: false as const,
items: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
}
},
poll: {
type: types.object,
optional: bool.true, nullable: bool.true,
type: 'object' as const,
optional: true as const, nullable: true as const,
},
geo: {
type: types.object,
optional: bool.true, nullable: bool.true,
type: 'object' as const,
optional: true as const, nullable: true as const,
},
},
};

View file

@ -3,7 +3,7 @@ import { Users, Notes } from '..';
import { Notification } from '../entities/notification';
import { ensure } from '../../prelude/ensure';
import { awaitAll } from '../../prelude/await-all';
import { types, bool, SchemaType } from '../../misc/schema';
import { SchemaType } from '../../misc/schema';
export type PackedNotification = SchemaType<typeof packedNotificationSchema>;
@ -51,37 +51,37 @@ export class NotificationRepository extends Repository<Notification> {
}
export const packedNotificationSchema = {
type: types.object,
optional: bool.false, nullable: bool.false,
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
description: 'The unique identifier for this notification.',
example: 'xxxxxxxxxx',
},
createdAt: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'date-time',
description: 'The date that the notification was created.'
},
type: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
enum: ['follow', 'receiveFollowRequest', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote'],
description: 'The type of the notification.'
},
userId: {
type: types.string,
optional: bool.true, nullable: bool.true,
type: 'string' as const,
optional: true as const, nullable: true as const,
format: 'id',
},
user: {
type: types.object,
type: 'object' as const,
ref: 'User',
optional: bool.true, nullable: bool.true,
optional: true as const, nullable: true as const,
},
}
};

View file

@ -1,6 +1,6 @@
import { EntityRepository, Repository } from 'typeorm';
import { Page } from '../entities/page';
import { SchemaType, types, bool } from '../../misc/schema';
import { SchemaType } from '../../misc/schema';
import { Users, DriveFiles, PageLikes } from '..';
import { awaitAll } from '../../prelude/await-all';
import { DriveFile } from '../entities/drive-file';
@ -89,54 +89,54 @@ export class PageRepository extends Repository<Page> {
}
export const packedPageSchema = {
type: types.object,
optional: bool.false, nullable: bool.false,
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
example: 'xxxxxxxxxx',
},
createdAt: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'date-time',
},
updatedAt: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'date-time',
},
title: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
},
name: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
},
summary: {
type: types.string,
optional: bool.false, nullable: bool.true,
type: 'string' as const,
optional: false as const, nullable: true as const,
},
content: {
type: types.array,
optional: bool.false, nullable: bool.false,
type: 'array' as const,
optional: false as const, nullable: false as const,
},
variables: {
type: types.array,
optional: bool.false, nullable: bool.false,
type: 'array' as const,
optional: false as const, nullable: false as const,
},
userId: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
},
user: {
type: types.object,
type: 'object' as const,
ref: 'User',
optional: bool.false, nullable: bool.false,
optional: false as const, nullable: false as const,
},
}
};

View file

@ -2,7 +2,7 @@ import { EntityRepository, Repository } from 'typeorm';
import { UserGroup } from '../entities/user-group';
import { ensure } from '../../prelude/ensure';
import { UserGroupJoinings } from '..';
import { bool, types, SchemaType } from '../../misc/schema';
import { SchemaType } from '../../misc/schema';
export type PackedUserGroup = SchemaType<typeof packedUserGroupSchema>;
@ -28,38 +28,38 @@ export class UserGroupRepository extends Repository<UserGroup> {
}
export const packedUserGroupSchema = {
type: types.object,
optional: bool.false, nullable: bool.false,
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
description: 'The unique identifier for this UserGroup.',
example: 'xxxxxxxxxx',
},
createdAt: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'date-time',
description: 'The date that the UserGroup was created.'
},
name: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
description: 'The name of the UserGroup.'
},
ownerId: {
type: types.string,
nullable: bool.false, optional: bool.false,
type: 'string' as const,
nullable: false as const, optional: false as const,
format: 'id',
},
userIds: {
type: types.array,
nullable: bool.false, optional: bool.true,
type: 'array' as const,
nullable: false as const, optional: true as const,
items: {
type: types.string,
nullable: bool.false, optional: bool.false,
type: 'string' as const,
nullable: false as const, optional: false as const,
format: 'id',
}
},

View file

@ -2,7 +2,7 @@ import { EntityRepository, Repository } from 'typeorm';
import { UserList } from '../entities/user-list';
import { ensure } from '../../prelude/ensure';
import { UserListJoinings } from '..';
import { bool, types, SchemaType } from '../../misc/schema';
import { SchemaType } from '../../misc/schema';
export type PackedUserList = SchemaType<typeof packedUserListSchema>;
@ -27,33 +27,33 @@ export class UserListRepository extends Repository<UserList> {
}
export const packedUserListSchema = {
type: types.object,
optional: bool.false, nullable: bool.false,
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
description: 'The unique identifier for this UserList.',
example: 'xxxxxxxxxx',
},
createdAt: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'date-time',
description: 'The date that the UserList was created.'
},
name: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
description: 'The name of the UserList.'
},
userIds: {
type: types.array,
nullable: bool.false, optional: bool.true,
type: 'array' as const,
nullable: false as const, optional: true as const,
items: {
type: types.string,
nullable: bool.false, optional: bool.false,
type: 'string' as const,
nullable: false as const, optional: false as const,
format: 'id',
}
},

View file

@ -1,10 +1,10 @@
import $ from 'cafy';
import { EntityRepository, Repository, In } from 'typeorm';
import { User, ILocalUser, IRemoteUser } from '../entities/user';
import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserGroupJoinings } from '..';
import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings } from '..';
import { ensure } from '../../prelude/ensure';
import config from '../../config';
import { SchemaType, bool, types } from '../../misc/schema';
import { SchemaType } from '../../misc/schema';
import { awaitAll } from '../../prelude/await-all';
export type PackedUser = SchemaType<typeof packedUserSchema>;
@ -156,6 +156,11 @@ export class UserRepository extends Repository<User> {
detail: true
}),
twoFactorEnabled: profile!.twoFactorEnabled,
securityKeys: profile!.twoFactorEnabled
? UserSecurityKeys.count({
userId: user.id
}).then(result => result >= 1)
: false,
twitter: profile!.twitter ? {
id: profile!.twitterUserId,
screenName: profile!.twitterScreenName
@ -195,6 +200,15 @@ export class UserRepository extends Repository<User> {
clientData: profile!.clientData,
email: profile!.email,
emailVerified: profile!.emailVerified,
securityKeysList: profile!.twoFactorEnabled
? UserSecurityKeys.find({
where: {
userId: user.id
},
select: ['id', 'name', 'lastUsed']
})
: []
} : {}),
...(relation ? {
@ -243,150 +257,150 @@ export class UserRepository extends Repository<User> {
}
export const packedUserSchema = {
type: types.object,
nullable: bool.false, optional: bool.false,
type: 'object' as const,
nullable: false as const, optional: false as const,
properties: {
id: {
type: types.string,
nullable: bool.false, optional: bool.false,
type: 'string' as const,
nullable: false as const, optional: false as const,
format: 'id',
description: 'The unique identifier for this User.',
example: 'xxxxxxxxxx',
},
username: {
type: types.string,
nullable: bool.false, optional: bool.false,
type: 'string' as const,
nullable: false as const, optional: false as const,
description: 'The screen name, handle, or alias that this user identifies themselves with.',
example: 'ai'
},
name: {
type: types.string,
nullable: bool.true, optional: bool.false,
type: 'string' as const,
nullable: true as const, optional: false as const,
description: 'The name of the user, as theyve defined it.',
example: '藍'
},
url: {
type: types.string,
type: 'string' as const,
format: 'url',
nullable: bool.true, optional: bool.true,
nullable: true as const, optional: true as const,
},
avatarUrl: {
type: types.string,
type: 'string' as const,
format: 'url',
nullable: bool.true, optional: bool.false,
nullable: true as const, optional: false as const,
},
avatarColor: {
type: types.any,
nullable: bool.true, optional: bool.false,
type: 'any' as const,
nullable: true as const, optional: false as const,
},
bannerUrl: {
type: types.string,
type: 'string' as const,
format: 'url',
nullable: bool.true, optional: bool.true,
nullable: true as const, optional: true as const,
},
bannerColor: {
type: types.any,
nullable: bool.true, optional: bool.true,
type: 'any' as const,
nullable: true as const, optional: true as const,
},
emojis: {
type: types.any,
nullable: bool.true, optional: bool.false,
type: 'any' as const,
nullable: true as const, optional: false as const,
},
host: {
type: types.string,
nullable: bool.true, optional: bool.false,
type: 'string' as const,
nullable: true as const, optional: false as const,
example: 'misskey.example.com'
},
description: {
type: types.string,
nullable: bool.true, optional: bool.true,
type: 'string' as const,
nullable: true as const, optional: true as const,
description: 'The user-defined UTF-8 string describing their account.',
example: 'Hi masters, I am Ai!'
},
birthday: {
type: types.string,
nullable: bool.true, optional: bool.true,
type: 'string' as const,
nullable: true as const, optional: true as const,
example: '2018-03-12'
},
createdAt: {
type: types.string,
nullable: bool.false, optional: bool.true,
type: 'string' as const,
nullable: false as const, optional: true as const,
format: 'date-time',
description: 'The date that the user account was created on Misskey.'
},
updatedAt: {
type: types.string,
nullable: bool.true, optional: bool.true,
type: 'string' as const,
nullable: true as const, optional: true as const,
format: 'date-time',
},
location: {
type: types.string,
nullable: bool.true, optional: bool.true,
type: 'string' as const,
nullable: true as const, optional: true as const,
},
followersCount: {
type: types.number,
nullable: bool.false, optional: bool.true,
type: 'number' as const,
nullable: false as const, optional: true as const,
description: 'The number of followers this account currently has.'
},
followingCount: {
type: types.number,
nullable: bool.false, optional: bool.true,
type: 'number' as const,
nullable: false as const, optional: true as const,
description: 'The number of users this account is following.'
},
notesCount: {
type: types.number,
nullable: bool.false, optional: bool.true,
type: 'number' as const,
nullable: false as const, optional: true as const,
description: 'The number of Notes (including renotes) issued by the user.'
},
isBot: {
type: types.boolean,
nullable: bool.false, optional: bool.true,
type: 'boolean' as const,
nullable: false as const, optional: true as const,
description: 'Whether this account is a bot.'
},
pinnedNoteIds: {
type: types.array,
nullable: bool.false, optional: bool.true,
type: 'array' as const,
nullable: false as const, optional: true as const,
items: {
type: types.string,
nullable: bool.false, optional: bool.false,
type: 'string' as const,
nullable: false as const, optional: false as const,
format: 'id',
}
},
pinnedNotes: {
type: types.array,
nullable: bool.false, optional: bool.true,
type: 'array' as const,
nullable: false as const, optional: true as const,
items: {
type: types.object,
nullable: bool.false, optional: bool.false,
type: 'object' as const,
nullable: false as const, optional: false as const,
ref: 'Note'
}
},
isCat: {
type: types.boolean,
nullable: bool.false, optional: bool.true,
type: 'boolean' as const,
nullable: false as const, optional: true as const,
description: 'Whether this account is a cat.'
},
isAdmin: {
type: types.boolean,
nullable: bool.false, optional: bool.true,
type: 'boolean' as const,
nullable: false as const, optional: true as const,
description: 'Whether this account is the admin.'
},
isModerator: {
type: types.boolean,
nullable: bool.false, optional: bool.true,
type: 'boolean' as const,
nullable: false as const, optional: true as const,
description: 'Whether this account is a moderator.'
},
isLocked: {
type: types.boolean,
nullable: bool.false, optional: bool.true,
type: 'boolean' as const,
nullable: false as const, optional: true as const,
},
hasUnreadSpecifiedNotes: {
type: types.boolean,
nullable: bool.false, optional: bool.true,
type: 'boolean' as const,
nullable: false as const, optional: true as const,
},
hasUnreadMentions: {
type: types.boolean,
nullable: bool.false, optional: bool.true,
type: 'boolean' as const,
nullable: false as const, optional: true as const,
},
},
};

View file

@ -1,19 +0,0 @@
import * as fs from 'fs';
import config from './config';
const json = {
type: 'postgres',
host: config.db.host,
port: config.db.port,
username: config.db.user,
password: config.db.pass,
database: config.db.db,
extra: config.db.extra,
entities: ['src/models/entities/*.ts'],
migrations: ['migration/*.ts'],
cli: {
migrationsDir: 'migration'
}
};
fs.writeFileSync('ormconfig.json', JSON.stringify(json));

View file

@ -194,6 +194,13 @@ export function createDeleteObjectStorageFileJob(key: string) {
});
}
export function createCleanRemoteFilesJob() {
return objectStorageQueue.add('cleanRemoteFiles', {}, {
removeOnComplete: true,
removeOnFail: true
});
}
export default function() {
if (!program.onlyServer) {
deliverQueue.process(128, processDeliver);

View file

@ -1,7 +1,7 @@
import * as Bull from 'bull';
import { queueLogger } from '../../logger';
import { deleteFile } from '../../../services/drive/delete-file';
import { deleteFileSync } from '../../../services/drive/delete-file';
import { Users, DriveFiles } from '../../../models';
import { MoreThan } from 'typeorm';
@ -39,7 +39,7 @@ export async function deleteDriveFiles(job: Bull.Job, done: any): Promise<void>
cursor = files[files.length - 1].id;
for (const file of files) {
await deleteFile(file);
await deleteFileSync(file);
deletedCount++;
}

View file

@ -0,0 +1,50 @@
import * as Bull from 'bull';
import { queueLogger } from '../../logger';
import { deleteFileSync } from '../../../services/drive/delete-file';
import { DriveFiles } from '../../../models';
import { MoreThan, Not, IsNull } from 'typeorm';
const logger = queueLogger.createSubLogger('clean-remote-files');
export default async function cleanRemoteFiles(job: Bull.Job, done: any): Promise<void> {
logger.info(`Deleting cached remote files...`);
let deletedCount = 0;
let cursor: any = null;
while (true) {
const files = await DriveFiles.find({
where: {
userHost: Not(IsNull()),
isLink: false,
...(cursor ? { id: MoreThan(cursor) } : {})
},
take: 8,
order: {
id: 1
}
});
if (files.length === 0) {
job.progress(100);
break;
}
cursor = files[files.length - 1].id;
await Promise.all(files.map(file => deleteFileSync(file, true)));
deletedCount += 8;
const total = await DriveFiles.count({
userHost: Not(IsNull()),
isLink: false,
});
job.progress(deletedCount / total);
}
logger.succ(`All cahced remote files has been deleted.`);
done();
}

View file

@ -1,22 +1,10 @@
import * as Bull from 'bull';
import * as Minio from 'minio';
import { fetchMeta } from '../../../misc/fetch-meta';
import { deleteObjectStorageFile } from '../../../services/drive/delete-file';
export default async (job: Bull.Job) => {
const meta = await fetchMeta();
const minio = new Minio.Client({
endPoint: meta.objectStorageEndpoint!,
region: meta.objectStorageRegion ? meta.objectStorageRegion : undefined,
port: meta.objectStoragePort ? meta.objectStoragePort : undefined,
useSSL: meta.objectStorageUseSSL,
accessKey: meta.objectStorageAccessKey!,
secretKey: meta.objectStorageSecretKey!,
});
const key: string = job.data.key;
await minio.removeObject(meta.objectStorageBucket!, key);
await deleteObjectStorageFile(key);
return 'Success';
};

View file

@ -1,12 +1,14 @@
import * as Bull from 'bull';
import deleteFile from './delete-file';
import cleanRemoteFiles from './clean-remote-files';
const jobs = {
deleteFile,
cleanRemoteFiles,
} as any;
export default function(q: Bull.Queue) {
for (const [k, v] of Object.entries(jobs)) {
q.process(k, v as any);
q.process(k, 16, v as any);
}
}

View file

@ -1,13 +1,13 @@
import Resolver from '../../resolver';
import { IRemoteUser } from '../../../../models/entities/user';
import announceNote from './note';
import { IAnnounce, INote } from '../../type';
import { IAnnounce, INote, validPost, getApId } from '../../type';
import { apLogger } from '../../logger';
const logger = apLogger;
export default async (actor: IRemoteUser, activity: IAnnounce): Promise<void> => {
const uri = activity.id || activity;
const uri = getApId(activity);
logger.info(`Announce: ${uri}`);
@ -22,15 +22,9 @@ export default async (actor: IRemoteUser, activity: IAnnounce): Promise<void> =>
throw e;
}
switch (object.type) {
case 'Note':
case 'Question':
case 'Article':
if (validPost.includes(object.type)) {
announceNote(resolver, actor, activity, object as INote);
break;
default:
} else {
logger.warn(`Unknown announce type: ${object.type}`);
break;
}
};

View file

@ -1,7 +1,7 @@
import Resolver from '../../resolver';
import post from '../../../../services/note/create';
import { IRemoteUser, User } from '../../../../models/entities/user';
import { IAnnounce, INote } from '../../type';
import { IAnnounce, INote, getApId, getApIds } from '../../type';
import { fetchNote, resolveNote } from '../../models/note';
import { resolvePerson } from '../../models/person';
import { apLogger } from '../../logger';
@ -14,17 +14,13 @@ const logger = apLogger;
*
*/
export default async function(resolver: Resolver, actor: IRemoteUser, activity: IAnnounce, note: INote): Promise<void> {
const uri = activity.id || activity;
const uri = getApId(activity);
// アナウンサーが凍結されていたらスキップ
if (actor.isSuspended) {
return;
}
if (typeof uri !== 'string') {
throw new Error('invalid announce');
}
// アナウンス先をブロックしてたら中断
const meta = await fetchMeta();
if (meta.blockedHosts.includes(extractDbHost(uri))) return;
@ -52,11 +48,14 @@ export default async function(resolver: Resolver, actor: IRemoteUser, activity:
logger.info(`Creating the (Re)Note: ${uri}`);
//#region Visibility
const visibility = getVisibility(activity.to || [], activity.cc || [], actor);
const to = getApIds(activity.to);
const cc = getApIds(activity.cc);
const visibility = getVisibility(to, cc, actor);
let visibleUsers: User[] = [];
if (visibility == 'specified') {
visibleUsers = await Promise.all((note.to || []).map(uri => resolvePerson(uri)));
visibleUsers = await Promise.all(to.map(uri => resolvePerson(uri)));
}
//#endergion

View file

@ -1,6 +0,0 @@
import { IRemoteUser } from '../../../../models/entities/user';
import { createImage } from '../../models/image';
export default async function(actor: IRemoteUser, image: any): Promise<void> {
await createImage(image.url, actor);
}

View file

@ -1,14 +1,13 @@
import Resolver from '../../resolver';
import { IRemoteUser } from '../../../../models/entities/user';
import createImage from './image';
import createNote from './note';
import { ICreate } from '../../type';
import { ICreate, getApId, validPost } from '../../type';
import { apLogger } from '../../logger';
const logger = apLogger;
export default async (actor: IRemoteUser, activity: ICreate): Promise<void> => {
const uri = activity.id || activity;
const uri = getApId(activity);
logger.info(`Create: ${uri}`);
@ -23,19 +22,9 @@ export default async (actor: IRemoteUser, activity: ICreate): Promise<void> => {
throw e;
}
switch (object.type) {
case 'Image':
createImage(actor, object);
break;
case 'Note':
case 'Question':
case 'Article':
if (validPost.includes(object.type)) {
createNote(resolver, actor, object);
break;
default:
} else {
logger.warn(`Unknown type: ${object.type}`);
break;
}
};

View file

@ -1,9 +1,8 @@
import Resolver from '../../resolver';
import deleteNote from './note';
import { IRemoteUser } from '../../../../models/entities/user';
import { IDelete } from '../../type';
import { IDelete, getApId, validPost } from '../../type';
import { apLogger } from '../../logger';
import { Notes } from '../../../../models';
/**
*
@ -17,24 +16,11 @@ export default async (actor: IRemoteUser, activity: IDelete): Promise<void> => {
const object = await resolver.resolve(activity.object);
const uri = (object as any).id;
const uri = getApId(object);
switch (object.type) {
case 'Note':
case 'Question':
case 'Article':
deleteNote(actor, uri);
break;
case 'Tombstone':
const note = await Notes.findOne({ uri });
if (note != null) {
deleteNote(actor, uri);
}
break;
default:
apLogger.warn(`Unknown type: ${object.type}`);
break;
if (validPost.includes(object.type) || object.type === 'Tombstone') {
deleteNote(actor, uri);
} else {
apLogger.warn(`Unknown type: ${object.type}`);
}
};

View file

@ -17,7 +17,7 @@ import { deliverQuestionUpdate } from '../../../services/note/polls/update';
import { extractDbHost, toPuny } from '../../../misc/convert-host';
import { Notes, Emojis, Polls } from '../../../models';
import { Note } from '../../../models/entities/note';
import { IObject, INote } from '../type';
import { IObject, INote, getApIds, getOneApId, getApId, validPost } from '../type';
import { Emoji } from '../../../models/entities/emoji';
import { genId } from '../../../misc/gen-id';
import { fetchMeta } from '../../../misc/fetch-meta';
@ -32,7 +32,7 @@ export function validateNote(object: any, uri: string) {
return new Error('invalid Note: object is null');
}
if (!['Note', 'Question', 'Article'].includes(object.type)) {
if (!validPost.includes(object.type)) {
return new Error(`invalid Note: invalied object type ${object.type}`);
}
@ -40,7 +40,7 @@ export function validateNote(object: any, uri: string) {
return new Error(`invalid Note: id has different host. expected: ${expectHost}, actual: ${extractDbHost(object.id)}`);
}
if (object.attributedTo && extractDbHost(object.attributedTo) !== expectHost) {
if (object.attributedTo && extractDbHost(getOneApId(object.attributedTo)) !== expectHost) {
return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${extractDbHost(object.attributedTo)}`);
}
@ -53,8 +53,7 @@ export function validateNote(object: any, uri: string) {
* Misskeyに対象のNoteが登録されていればそれを返します
*/
export async function fetchNote(value: string | IObject, resolver?: Resolver): Promise<Note | null> {
const uri = typeof value == 'string' ? value : value.id;
if (uri == null) throw new Error('missing uri');
const uri = getApId(value);
// URIがこのサーバーを指しているならデータベースからフェッチ
if (uri.startsWith(config.url + '/')) {
@ -76,12 +75,12 @@ export async function fetchNote(value: string | IObject, resolver?: Resolver): P
/**
* Noteを作成します
*/
export async function createNote(value: any, resolver?: Resolver, silent = false): Promise<Note | null> {
export async function createNote(value: string | IObject, resolver?: Resolver, silent = false): Promise<Note | null> {
if (resolver == null) resolver = new Resolver();
const object: any = await resolver.resolve(value);
const entryUri = value.id || value;
const entryUri = getApId(value);
const err = validateNote(object, entryUri);
if (err) {
logger.error(`${err.message}`, {
@ -101,7 +100,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
logger.info(`Creating the Note: ${note.id}`);
// 投稿者をフェッチ
const actor = await resolvePerson(note.attributedTo, resolver) as IRemoteUser;
const actor = await resolvePerson(getOneApId(note.attributedTo), resolver) as IRemoteUser;
// 投稿者が凍結されていたらスキップ
if (actor.isSuspended) {
@ -109,24 +108,24 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
}
//#region Visibility
note.to = note.to == null ? [] : typeof note.to == 'string' ? [note.to] : note.to;
note.cc = note.cc == null ? [] : typeof note.cc == 'string' ? [note.cc] : note.cc;
const to = getApIds(note.to);
const cc = getApIds(note.cc);
let visibility = 'public';
let visibleUsers: User[] = [];
if (!note.to.includes('https://www.w3.org/ns/activitystreams#Public')) {
if (note.cc.includes('https://www.w3.org/ns/activitystreams#Public')) {
if (!to.includes('https://www.w3.org/ns/activitystreams#Public')) {
if (cc.includes('https://www.w3.org/ns/activitystreams#Public')) {
visibility = 'home';
} else if (note.to.includes(`${actor.uri}/followers`)) { // TODO: person.followerと照合するべき
} else if (to.includes(`${actor.uri}/followers`)) { // TODO: person.followerと照合するべき
visibility = 'followers';
} else {
visibility = 'specified';
visibleUsers = await Promise.all(note.to.map(uri => resolvePerson(uri, resolver)));
visibleUsers = await Promise.all(to.map(uri => resolvePerson(uri, resolver)));
}
}
//#endergion
const apMentions = await extractMentionedUsers(actor, note.to, note.cc, resolver);
const apMentions = await extractMentionedUsers(actor, to, cc, resolver);
const apHashtags = await extractHashtags(note.tag);
@ -217,11 +216,11 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
const apEmojis = emojis.map(emoji => emoji.name);
const questionUri = note._misskey_question;
const poll = await extractPollFromQuestion(note._misskey_question || note).catch(() => undefined);
const poll = await extractPollFromQuestion(note._misskey_question || note, resolver).catch(() => undefined);
// ユーザーの情報が古かったらついでに更新しておく
if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
updatePerson(note.attributedTo);
if (actor.uri) updatePerson(actor.uri);
}
return await post(actor, {

View file

@ -1,12 +1,19 @@
import config from '../../../config';
import Resolver from '../resolver';
import { IQuestion } from '../type';
import { IObject, IQuestion, isQuestion, } from '../type';
import { apLogger } from '../logger';
import { Notes, Polls } from '../../../models';
import { IPoll } from '../../../models/entities/poll';
export async function extractPollFromQuestion(source: string | IQuestion): Promise<IPoll> {
const question = typeof source === 'string' ? await new Resolver().resolve(source) as IQuestion : source;
export async function extractPollFromQuestion(source: string | IObject, resolver?: Resolver): Promise<IPoll> {
if (resolver == null) resolver = new Resolver();
const question = await resolver.resolve(source);
if (!isQuestion(question)) {
throw new Error('invalid type');
}
const multiple = !question.oneOf;
const expiresAt = question.endTime ? new Date(question.endTime) : null;

View file

@ -6,9 +6,9 @@ export interface IObject {
id?: string;
summary?: string;
published?: string;
cc?: string[];
to?: string[];
attributedTo: string;
cc?: IObject | string | (IObject | string)[];
to?: IObject | string | (IObject | string)[];
attributedTo: IObject | string | (IObject | string)[];
attachment?: any[];
inReplyTo?: any;
replies?: ICollection;
@ -23,6 +23,32 @@ export interface IObject {
sensitive?: boolean;
}
/**
* Get array of ActivityStreams Objects id
*/
export function getApIds(value: IObject | string | (IObject | string)[] | undefined): string[] {
if (value == null) return [];
const array = Array.isArray(value) ? value : [value];
return array.map(x => getApId(x));
}
/**
* Get first ActivityStreams Object id
*/
export function getOneApId(value: IObject | string | (IObject | string)[]): string {
const firstOne = Array.isArray(value) ? value[0] : value;
return getApId(firstOne);
}
/**
* Get ActivityStreams Object id
*/
export function getApId(value: string | IObject): string {
if (typeof value === 'string') return value;
if (typeof value.id === 'string') return value.id;
throw new Error(`cannot detemine id`);
}
export interface IActivity extends IObject {
//type: 'Activity';
actor: IObject | string;
@ -42,8 +68,10 @@ export interface IOrderedCollection extends IObject {
orderedItems: IObject | string | IObject[] | string[];
}
export const validPost = ['Note', 'Question', 'Article', 'Audio', 'Document', 'Image', 'Page', 'Video'];
export interface INote extends IObject {
type: 'Note' | 'Question';
type: 'Note' | 'Question' | 'Article' | 'Audio' | 'Document' | 'Image' | 'Page' | 'Video';
_misskey_content?: string;
_misskey_quote?: string;
_misskey_question?: string;
@ -59,6 +87,9 @@ export interface IQuestion extends IObject {
endTime?: Date;
}
export const isQuestion = (object: IObject): object is IQuestion =>
object.type === 'Note' || object.type === 'Question';
interface IQuestionChoice {
name?: string;
replies?: ICollection;

422
src/server/api/2fa.ts Normal file
View file

@ -0,0 +1,422 @@
import * as crypto from 'crypto';
import config from '../../config';
import * as jsrsasign from 'jsrsasign';
const ECC_PRELUDE = Buffer.from([0x04]);
const NULL_BYTE = Buffer.from([0]);
const PEM_PRELUDE = Buffer.from(
'3059301306072a8648ce3d020106082a8648ce3d030107034200',
'hex'
);
// Android Safetynet attestations are signed with this cert:
const GSR2 = `-----BEGIN CERTIFICATE-----
MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4G
A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNp
Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1
MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEG
A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI
hvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6ErPL
v4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8
eoLrvozps6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklq
tTleiDTsvHgMCJiEbKjNS7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzd
C9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pa
zq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCB
mTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm+IH
V2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5n
bG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG
3lm0mi3f3BmGLjANBgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4Gs
J0/WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO
291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavS
ot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG79G+dwfCMNYxd
AfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7
TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg==
-----END CERTIFICATE-----\n`;
function base64URLDecode(source: string) {
return Buffer.from(source.replace(/\-/g, '+').replace(/_/g, '/'), 'base64');
}
function getCertSubject(certificate: string) {
const subjectCert = new jsrsasign.X509();
subjectCert.readCertPEM(certificate);
const subjectString = subjectCert.getSubjectString();
const subjectFields = subjectString.slice(1).split('/');
const fields = {} as Record<string, string>;
for (const field of subjectFields) {
const eqIndex = field.indexOf('=');
fields[field.substring(0, eqIndex)] = field.substring(eqIndex + 1);
}
return fields;
}
function verifyCertificateChain(certificates: string[]) {
let valid = true;
for (let i = 0; i < certificates.length; i++) {
const Cert = certificates[i];
const certificate = new jsrsasign.X509();
certificate.readCertPEM(Cert);
const CACert = i + 1 >= certificates.length ? Cert : certificates[i + 1];
const certStruct = jsrsasign.ASN1HEX.getTLVbyList(certificate.hex!, 0, [0]);
const algorithm = certificate.getSignatureAlgorithmField();
const signatureHex = certificate.getSignatureValueHex();
// Verify against CA
const Signature = new jsrsasign.KJUR.crypto.Signature({alg: algorithm});
Signature.init(CACert);
Signature.updateHex(certStruct);
valid = valid && !!Signature.verify(signatureHex); // true if CA signed the certificate
}
return valid;
}
function PEMString(pemBuffer: Buffer, type = 'CERTIFICATE') {
if (pemBuffer.length == 65 && pemBuffer[0] == 0x04) {
pemBuffer = Buffer.concat([PEM_PRELUDE, pemBuffer], 91);
type = 'PUBLIC KEY';
}
const cert = pemBuffer.toString('base64');
const keyParts = [];
const max = Math.ceil(cert.length / 64);
let start = 0;
for (let i = 0; i < max; i++) {
keyParts.push(cert.substring(start, start + 64));
start += 64;
}
return (
`-----BEGIN ${type}-----\n` +
keyParts.join('\n') +
`\n-----END ${type}-----\n`
);
}
export function hash(data: Buffer) {
return crypto
.createHash('sha256')
.update(data)
.digest();
}
export function verifyLogin({
publicKey,
authenticatorData,
clientDataJSON,
clientData,
signature,
challenge
}: {
publicKey: Buffer,
authenticatorData: Buffer,
clientDataJSON: Buffer,
clientData: any,
signature: Buffer,
challenge: string
}) {
if (clientData.type != 'webauthn.get') {
throw new Error('type is not webauthn.get');
}
if (hash(clientData.challenge).toString('hex') != challenge) {
throw new Error('challenge mismatch');
}
if (clientData.origin != config.scheme + '://' + config.host) {
throw new Error('origin mismatch');
}
const verificationData = Buffer.concat(
[authenticatorData, hash(clientDataJSON)],
32 + authenticatorData.length
);
return crypto
.createVerify('SHA256')
.update(verificationData)
.verify(PEMString(publicKey), signature);
}
export const procedures = {
none: {
verify({publicKey}: {publicKey: Map<number, Buffer>}) {
const negTwo = publicKey.get(-2);
if (!negTwo || negTwo.length != 32) {
throw new Error('invalid or no -2 key given');
}
const negThree = publicKey.get(-3);
if (!negThree || negThree.length != 32) {
throw new Error('invalid or no -3 key given');
}
const publicKeyU2F = Buffer.concat(
[ECC_PRELUDE, negTwo, negThree],
1 + 32 + 32
);
return {
publicKey: publicKeyU2F,
valid: true
};
}
},
'android-key': {
verify({
attStmt,
authenticatorData,
clientDataHash,
publicKey,
rpIdHash,
credentialId
}: {
attStmt: any,
authenticatorData: Buffer,
clientDataHash: Buffer,
publicKey: Map<number, any>;
rpIdHash: Buffer,
credentialId: Buffer,
}) {
if (attStmt.alg != -7) {
throw new Error('alg mismatch');
}
const verificationData = Buffer.concat([
authenticatorData,
clientDataHash
]);
const attCert: Buffer = attStmt.x5c[0];
const negTwo = publicKey.get(-2);
if (!negTwo || negTwo.length != 32) {
throw new Error('invalid or no -2 key given');
}
const negThree = publicKey.get(-3);
if (!negThree || negThree.length != 32) {
throw new Error('invalid or no -3 key given');
}
const publicKeyData = Buffer.concat(
[ECC_PRELUDE, negTwo, negThree],
1 + 32 + 32
);
if (!attCert.equals(publicKeyData)) {
throw new Error('public key mismatch');
}
const isValid = crypto
.createVerify('SHA256')
.update(verificationData)
.verify(PEMString(attCert), attStmt.sig);
// TODO: Check 'attestationChallenge' field in extension of cert matches hash(clientDataJSON)
return {
valid: isValid,
publicKey: publicKeyData
};
}
},
// what a stupid attestation
'android-safetynet': {
verify({
attStmt,
authenticatorData,
clientDataHash,
publicKey,
rpIdHash,
credentialId
}: {
attStmt: any,
authenticatorData: Buffer,
clientDataHash: Buffer,
publicKey: Map<number, any>;
rpIdHash: Buffer,
credentialId: Buffer,
}) {
const verificationData = hash(
Buffer.concat([authenticatorData, clientDataHash])
);
const jwsParts = attStmt.response.toString('utf-8').split('.');
const header = JSON.parse(base64URLDecode(jwsParts[0]).toString('utf-8'));
const response = JSON.parse(
base64URLDecode(jwsParts[1]).toString('utf-8')
);
const signature = jwsParts[2];
if (!verificationData.equals(Buffer.from(response.nonce, 'base64'))) {
throw new Error('invalid nonce');
}
const certificateChain = header.x5c
.map((key: any) => PEMString(key))
.concat([GSR2]);
if (getCertSubject(certificateChain[0]).CN != 'attest.android.com') {
throw new Error('invalid common name');
}
if (!verifyCertificateChain(certificateChain)) {
throw new Error('Invalid certificate chain!');
}
const signatureBase = Buffer.from(
jwsParts[0] + '.' + jwsParts[1],
'utf-8'
);
const valid = crypto
.createVerify('sha256')
.update(signatureBase)
.verify(certificateChain[0], base64URLDecode(signature));
const negTwo = publicKey.get(-2);
if (!negTwo || negTwo.length != 32) {
throw new Error('invalid or no -2 key given');
}
const negThree = publicKey.get(-3);
if (!negThree || negThree.length != 32) {
throw new Error('invalid or no -3 key given');
}
const publicKeyData = Buffer.concat(
[ECC_PRELUDE, negTwo, negThree],
1 + 32 + 32
);
return {
valid,
publicKey: publicKeyData
};
}
},
packed: {
verify({
attStmt,
authenticatorData,
clientDataHash,
publicKey,
rpIdHash,
credentialId
}: {
attStmt: any,
authenticatorData: Buffer,
clientDataHash: Buffer,
publicKey: Map<number, any>;
rpIdHash: Buffer,
credentialId: Buffer,
}) {
const verificationData = Buffer.concat([
authenticatorData,
clientDataHash
]);
if (attStmt.x5c) {
const attCert = attStmt.x5c[0];
const validSignature = crypto
.createVerify('SHA256')
.update(verificationData)
.verify(PEMString(attCert), attStmt.sig);
const negTwo = publicKey.get(-2);
if (!negTwo || negTwo.length != 32) {
throw new Error('invalid or no -2 key given');
}
const negThree = publicKey.get(-3);
if (!negThree || negThree.length != 32) {
throw new Error('invalid or no -3 key given');
}
const publicKeyData = Buffer.concat(
[ECC_PRELUDE, negTwo, negThree],
1 + 32 + 32
);
return {
valid: validSignature,
publicKey: publicKeyData
};
} else if (attStmt.ecdaaKeyId) {
// https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-ecdaa-algorithm-v2.0-id-20180227.html#ecdaa-verify-operation
throw new Error('ECDAA-Verify is not supported');
} else {
if (attStmt.alg != -7) throw new Error('alg mismatch');
throw new Error('self attestation is not supported');
}
}
},
'fido-u2f': {
verify({
attStmt,
authenticatorData,
clientDataHash,
publicKey,
rpIdHash,
credentialId
}: {
attStmt: any,
authenticatorData: Buffer,
clientDataHash: Buffer,
publicKey: Map<number, any>,
rpIdHash: Buffer,
credentialId: Buffer
}) {
const x5c: Buffer[] = attStmt.x5c;
if (x5c.length != 1) {
throw new Error('x5c length does not match expectation');
}
const attCert = x5c[0];
// TODO: make sure attCert is an Elliptic Curve (EC) public key over the P-256 curve
const negTwo: Buffer = publicKey.get(-2);
if (!negTwo || negTwo.length != 32) {
throw new Error('invalid or no -2 key given');
}
const negThree: Buffer = publicKey.get(-3);
if (!negThree || negThree.length != 32) {
throw new Error('invalid or no -3 key given');
}
const publicKeyU2F = Buffer.concat(
[ECC_PRELUDE, negTwo, negThree],
1 + 32 + 32
);
const verificationData = Buffer.concat([
NULL_BYTE,
rpIdHash,
clientDataHash,
credentialId,
publicKeyU2F
]);
const validSignature = crypto
.createVerify('SHA256')
.update(verificationData)
.verify(PEMString(attCert), attStmt.sig);
return {
valid: validSignature,
publicKey: publicKeyU2F
};
}
}
};

View file

@ -1,7 +1,5 @@
import { Not, IsNull } from 'typeorm';
import define from '../../../define';
import { deleteFile } from '../../../../../services/drive/delete-file';
import { DriveFiles } from '../../../../../models';
import { createCleanRemoteFilesJob } from '../../../../../queue';
export const meta = {
tags: ['admin'],
@ -11,12 +9,5 @@ export const meta = {
};
export default define(meta, async (ps, me) => {
const files = await DriveFiles.find({
userHost: Not(IsNull()),
isLink: false,
});
for (const file of files) {
deleteFile(file, true);
}
createCleanRemoteFilesJob();
});

View file

@ -0,0 +1,26 @@
import $ from 'cafy';
import define from '../../define';
import { sendEmail } from '../../../../services/send-email';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireModerator: true,
params: {
to: {
validator: $.str,
},
subject: {
validator: $.str,
},
text: {
validator: $.str,
},
}
};
export default define(meta, async (ps) => {
await sendEmail(ps.to, ps.subject, ps.text);
});

View file

@ -0,0 +1,33 @@
import $ from 'cafy';
import define from '../../define';
import { getConnection } from 'typeorm';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireModerator: true,
params: {
full: {
validator: $.bool,
},
analyze: {
validator: $.bool,
},
}
};
export default define(meta, async (ps) => {
const params: string[] = [];
if (ps.full) {
params.push('FULL');
}
if (ps.analyze) {
params.push('ANALYZE');
}
getConnection().query('VACUUM ' + params.join(' '));
});

View file

@ -10,7 +10,7 @@ import { Users, Notes } from '../../../../models';
import { Note } from '../../../../models/entities/note';
import { User } from '../../../../models/entities/user';
import { fetchMeta } from '../../../../misc/fetch-meta';
import { validActor } from '../../../../remote/activitypub/type';
import { validActor, validPost } from '../../../../remote/activitypub/type';
export const meta = {
tags: ['federation'],
@ -145,7 +145,7 @@ async function fetchAny(uri: string) {
};
}
if (['Note', 'Question', 'Article'].includes(object.type)) {
if (validPost.includes(object.type)) {
const note = await createNote(object.id, undefined, true);
return {
type: 'Note',

View file

@ -4,7 +4,6 @@ import define from '../../define';
import { Apps } from '../../../../models';
import { genId } from '../../../../misc/gen-id';
import { unique } from '../../../../prelude/array';
import { types, bool } from '../../../../misc/schema';
export const meta = {
tags: ['app'],
@ -53,8 +52,8 @@ export const meta = {
},
res: {
type: types.object,
optional: bool.false, nullable: bool.false,
type: 'object' as const,
optional: false as const, nullable: false as const,
ref: 'App',
},
};

View file

@ -3,7 +3,6 @@ import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import { ApiError } from '../../error';
import { Apps } from '../../../../models';
import { types, bool } from '../../../../misc/schema';
export const meta = {
tags: ['app'],
@ -15,8 +14,8 @@ export const meta = {
},
res: {
type: types.object,
optional: bool.false, nullable: bool.false,
type: 'object' as const,
optional: false as const, nullable: false as const,
ref: 'App',
},

View file

@ -5,7 +5,6 @@ import define from '../../../define';
import { ApiError } from '../../../error';
import { Apps, AuthSessions } from '../../../../../models';
import { genId } from '../../../../../misc/gen-id';
import { types, bool } from '../../../../../misc/schema';
export const meta = {
tags: ['auth'],
@ -28,17 +27,17 @@ export const meta = {
},
res: {
type: types.object,
optional: bool.false, nullable: bool.false,
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
token: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
description: 'セッションのトークン'
},
url: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'url',
description: 'セッションのURL'
},

View file

@ -3,7 +3,6 @@ import define from '../../../define';
import { ApiError } from '../../../error';
import { Apps, AuthSessions, AccessTokens, Users } from '../../../../../models';
import { ensure } from '../../../../../prelude/ensure';
import { types, bool } from '../../../../../misc/schema';
export const meta = {
tags: ['auth'],
@ -29,18 +28,18 @@ export const meta = {
},
res: {
type: types.object,
optional: bool.false, nullable: bool.false,
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
accessToken: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
description: 'ユーザーのアクセストークン',
},
user: {
type: types.object,
optional: bool.false, nullable: bool.false,
type: 'object' as const,
optional: false as const, nullable: false as const,
ref: 'User',
description: '認証したユーザー'
},

View file

@ -3,7 +3,6 @@ import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import { Blockings } from '../../../../models';
import { makePaginationQuery } from '../../common/make-pagination-query';
import { types, bool } from '../../../../misc/schema';
export const meta = {
desc: {
@ -33,11 +32,11 @@ export const meta = {
},
res: {
type: types.array,
optional: bool.false, nullable: bool.false,
type: 'array' as const,
optional: false as const, nullable: false as const,
items: {
type: types.object,
optional: bool.false, nullable: bool.false,
type: 'object' as const,
optional: false as const, nullable: false as const,
ref: 'Blocking',
}
},

View file

@ -1,7 +1,6 @@
import define from '../define';
import { fetchMeta } from '../../../misc/fetch-meta';
import { DriveFiles } from '../../../models';
import { types, bool } from '../../../misc/schema';
export const meta = {
desc: {
@ -16,16 +15,16 @@ export const meta = {
kind: 'read:drive',
res: {
type: types.object,
optional: bool.false, nullable: bool.false,
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
capacity: {
type: types.number,
optional: bool.false, nullable: bool.false,
type: 'number' as const,
optional: false as const, nullable: false as const,
},
usage: {
type: types.number,
optional: bool.false, nullable: bool.false,
type: 'number' as const,
optional: false as const, nullable: false as const,
}
}
}

View file

@ -3,7 +3,6 @@ import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import { DriveFiles } from '../../../../models';
import { makePaginationQuery } from '../../common/make-pagination-query';
import { types, bool } from '../../../../misc/schema';
export const meta = {
desc: {
@ -42,11 +41,11 @@ export const meta = {
},
res: {
type: types.array,
optional: bool.false, nullable: bool.false,
type: 'array' as const,
optional: false as const, nullable: false as const,
items: {
type: types.object,
optional: bool.false, nullable: bool.false,
type: 'object' as const,
optional: false as const, nullable: false as const,
ref: 'DriveFile',
}
},

View file

@ -3,7 +3,6 @@ import { ID } from '../../../../../misc/cafy-id';
import define from '../../../define';
import { ApiError } from '../../../error';
import { DriveFiles, Notes } from '../../../../../models';
import { types, bool } from '../../../../../misc/schema';
export const meta = {
stability: 'stable',
@ -30,11 +29,11 @@ export const meta = {
},
res: {
type: types.array,
optional: bool.false, nullable: bool.false,
type: 'array' as const,
optional: false as const, nullable: false as const,
items: {
type: types.object,
optional: bool.false, nullable: bool.false,
type: 'object' as const,
optional: false as const, nullable: false as const,
ref: 'Note',
}
},

View file

@ -1,7 +1,6 @@
import $ from 'cafy';
import define from '../../../define';
import { DriveFiles } from '../../../../../models';
import { types, bool } from '../../../../../misc/schema';
export const meta = {
desc: {
@ -25,8 +24,8 @@ export const meta = {
},
res: {
type: types.boolean,
optional: bool.false, nullable: bool.false,
type: 'boolean' as const,
optional: false as const, nullable: false as const,
},
};

View file

@ -6,7 +6,6 @@ import define from '../../../define';
import { apiLogger } from '../../../logger';
import { ApiError } from '../../../error';
import { DriveFiles } from '../../../../../models';
import { types, bool } from '../../../../../misc/schema';
export const meta = {
desc: {
@ -57,8 +56,8 @@ export const meta = {
},
res: {
type: types.object,
optional: bool.false, nullable: bool.false,
type: 'object' as const,
optional: false as const, nullable: false as const,
ref: 'DriveFile',
},

View file

@ -1,7 +1,6 @@
import $ from 'cafy';
import define from '../../../define';
import { DriveFiles } from '../../../../../models';
import { types, bool } from '../../../../../misc/schema';
export const meta = {
desc: {
@ -24,11 +23,11 @@ export const meta = {
},
res: {
type: types.array,
optional: bool.false, nullable: bool.false,
type: 'array' as const,
optional: false as const, nullable: false as const,
items: {
type: types.object,
optional: bool.false, nullable: bool.false,
type: 'object' as const,
optional: false as const, nullable: false as const,
ref: 'DriveFile',
}
},

View file

@ -2,7 +2,6 @@ import $ from 'cafy';
import { ID } from '../../../../../misc/cafy-id';
import define from '../../../define';
import { DriveFiles } from '../../../../../models';
import { types, bool } from '../../../../../misc/schema';
export const meta = {
requireCredential: true,
@ -26,11 +25,11 @@ export const meta = {
},
res: {
type: types.array,
optional: bool.false, nullable: bool.false,
type: 'array' as const,
optional: false as const, nullable: false as const,
items: {
type: types.object,
optional: bool.false, nullable: bool.false,
type: 'object' as const,
optional: false as const, nullable: false as const,
ref: 'DriveFile',
}
},

View file

@ -4,7 +4,6 @@ import define from '../../../define';
import { ApiError } from '../../../error';
import { DriveFile } from '../../../../../models/entities/drive-file';
import { DriveFiles } from '../../../../../models';
import { types, bool } from '../../../../../misc/schema';
export const meta = {
stability: 'stable',
@ -39,8 +38,8 @@ export const meta = {
},
res: {
type: types.object,
optional: bool.false, nullable: bool.false,
type: 'object' as const,
optional: false as const, nullable: false as const,
ref: 'DriveFile',
},

View file

@ -3,7 +3,6 @@ import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import { DriveFolders } from '../../../../models';
import { makePaginationQuery } from '../../common/make-pagination-query';
import { types, bool } from '../../../../misc/schema';
export const meta = {
desc: {
@ -38,11 +37,11 @@ export const meta = {
},
res: {
type: types.array,
optional: bool.false, nullable: bool.false,
type: 'array' as const,
optional: false as const, nullable: false as const,
items: {
type: types.object,
optional: bool.false, nullable: bool.false,
type: 'object' as const,
optional: false as const, nullable: false as const,
ref: 'DriveFolder',
}
},

View file

@ -2,7 +2,6 @@ import $ from 'cafy';
import { ID } from '../../../../../misc/cafy-id';
import define from '../../../define';
import { DriveFolders } from '../../../../../models';
import { types, bool } from '../../../../../misc/schema';
export const meta = {
tags: ['drive'],
@ -26,11 +25,11 @@ export const meta = {
},
res: {
type: types.array,
optional: bool.false, nullable: bool.false,
type: 'array' as const,
optional: false as const, nullable: false as const,
items: {
type: types.object,
optional: bool.false, nullable: bool.false,
type: 'object' as const,
optional: false as const, nullable: false as const,
ref: 'DriveFolder',
}
},

View file

@ -3,7 +3,6 @@ import { ID } from '../../../../../misc/cafy-id';
import define from '../../../define';
import { ApiError } from '../../../error';
import { DriveFolders } from '../../../../../models';
import { types, bool } from '../../../../../misc/schema';
export const meta = {
stability: 'stable',
@ -30,8 +29,8 @@ export const meta = {
},
res: {
type: types.object,
optional: bool.false, nullable: bool.false,
type: 'object' as const,
optional: false as const, nullable: false as const,
ref: 'DriveFolder',
},

View file

@ -3,7 +3,6 @@ import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import { DriveFiles } from '../../../../models';
import { makePaginationQuery } from '../../common/make-pagination-query';
import { types, bool } from '../../../../misc/schema';
export const meta = {
tags: ['drive'],
@ -32,11 +31,11 @@ export const meta = {
},
res: {
type: types.array,
optional: bool.false, nullable: bool.false,
type: 'array' as const,
optional: false as const, nullable: false as const,
items: {
type: types.object,
optional: bool.false, nullable: bool.false,
type: 'object' as const,
optional: false as const, nullable: false as const,
ref: 'DriveFile',
}
},

View file

@ -1,7 +1,6 @@
import $ from 'cafy';
import define from '../../define';
import { Hashtags } from '../../../../models';
import { types, bool } from '../../../../misc/schema';
export const meta = {
tags: ['hashtags'],
@ -48,11 +47,11 @@ export const meta = {
},
res: {
type: types.array,
optional: bool.false, nullable: bool.false,
type: 'array' as const,
optional: false as const, nullable: false as const,
items: {
type: types.object,
optional: bool.false, nullable: bool.false,
type: 'object' as const,
optional: false as const, nullable: false as const,
ref: 'Hashtag',
}
},

View file

@ -1,7 +1,6 @@
import $ from 'cafy';
import define from '../../define';
import { Hashtags } from '../../../../models';
import { types, bool } from '../../../../misc/schema';
export const meta = {
desc: {
@ -38,11 +37,11 @@ export const meta = {
},
res: {
type: types.array,
optional: bool.false, nullable: bool.false,
type: 'array' as const,
optional: false as const, nullable: false as const,
items: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
}
},
};

View file

@ -2,7 +2,6 @@ import $ from 'cafy';
import define from '../../define';
import { ApiError } from '../../error';
import { Hashtags } from '../../../../models';
import { types, bool } from '../../../../misc/schema';
export const meta = {
desc: {
@ -24,8 +23,8 @@ export const meta = {
},
res: {
type: types.object,
optional: bool.false, nullable: bool.false,
type: 'object' as const,
optional: false as const, nullable: false as const,
ref: 'Hashtag',
},

View file

@ -2,7 +2,6 @@ import define from '../../define';
import { fetchMeta } from '../../../../misc/fetch-meta';
import { Notes } from '../../../../models';
import { Note } from '../../../../models/entities/note';
import { types, bool } from '../../../../misc/schema';
/*
a分間のユニーク投稿数が今からa分前b分前の間のユニーク投稿数のn倍以上5
@ -24,27 +23,27 @@ export const meta = {
requireCredential: false,
res: {
type: types.array,
optional: bool.false, nullable: bool.false,
type: 'array' as const,
optional: false as const, nullable: false as const,
items: {
type: types.object,
optional: bool.false, nullable: bool.false,
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
tag: {
type: types.string,
optional: bool.false, nullable: bool.false,
type: 'string' as const,
optional: false as const, nullable: false as const,
},
chart: {
type: types.array,
optional: bool.false, nullable: bool.false,
type: 'array' as const,
optional: false as const, nullable: false as const,
items: {
type: types.number,
optional: bool.false, nullable: bool.false,
type: 'number' as const,
optional: false as const, nullable: false as const,
}
},
usersCount: {
type: types.number,
optional: bool.false, nullable: bool.false,
type: 'number' as const,
optional: false as const, nullable: false as const,
}
}
}

View file

@ -1,7 +1,6 @@
import $ from 'cafy';
import define from '../../define';
import { Users } from '../../../../models';
import { types, bool } from '../../../../misc/schema';
export const meta = {
requireCredential: false,
@ -48,11 +47,11 @@ export const meta = {
},
res: {
type: types.array,
optional: bool.false, nullable: bool.false,
type: 'array' as const,
optional: false as const, nullable: false as const,
items: {
type: types.object,
optional: bool.false, nullable: bool.false,
type: 'object' as const,
optional: false as const, nullable: false as const,
ref: 'User',
}
},

View file

@ -1,6 +1,5 @@
import define from '../define';
import { Users } from '../../../models';
import { types, bool } from '../../../misc/schema';
export const meta = {
stability: 'stable',
@ -16,8 +15,8 @@ export const meta = {
params: {},
res: {
type: types.object,
optional: bool.false, nullable: bool.false,
type: 'object' as const,
optional: false as const, nullable: false as const,
ref: 'User',
},
};

View file

@ -0,0 +1,67 @@
import $ from 'cafy';
import * as bcrypt from 'bcryptjs';
import * as crypto from 'crypto';
import define from '../../../define';
import { UserProfiles, UserSecurityKeys, AttestationChallenges } from '../../../../../models';
import { ensure } from '../../../../../prelude/ensure';
import { promisify } from 'util';
import { hash } from '../../../2fa';
import { genId } from '../../../../../misc/gen-id';
export const meta = {
requireCredential: true,
secure: true,
params: {
password: {
validator: $.str
}
}
};
const randomBytes = promisify(crypto.randomBytes);
export default define(meta, async (ps, user) => {
const profile = await UserProfiles.findOne(user.id).then(ensure);
// Compare password
const same = await bcrypt.compare(ps.password, profile.password!);
if (!same) {
throw new Error('incorrect password');
}
const keys = await UserSecurityKeys.find({
userId: user.id
});
if (keys.length === 0) {
throw new Error('no keys found');
}
// 32 byte challenge
const entropy = await randomBytes(32);
const challenge = entropy.toString('base64')
.replace(/=/g, '')
.replace(/\+/g, '-')
.replace(/\//g, '_');
const challengeId = genId();
await AttestationChallenges.save({
userId: user.id,
id: challengeId,
challenge: hash(Buffer.from(challenge, 'utf-8')).toString('hex'),
createdAt: new Date(),
registrationChallenge: false
});
return {
challenge,
challengeId,
securityKeys: keys.map(key => ({
id: key.id
}))
};
});

View file

@ -0,0 +1,151 @@
import $ from 'cafy';
import * as bcrypt from 'bcryptjs';
import { promisify } from 'util';
import * as cbor from 'cbor';
import define from '../../../define';
import {
UserProfiles,
UserSecurityKeys,
AttestationChallenges,
Users
} from '../../../../../models';
import { ensure } from '../../../../../prelude/ensure';
import config from '../../../../../config';
import { procedures, hash } from '../../../2fa';
import { publishMainStream } from '../../../../../services/stream';
const cborDecodeFirst = promisify(cbor.decodeFirst) as any;
export const meta = {
requireCredential: true,
secure: true,
params: {
clientDataJSON: {
validator: $.str
},
attestationObject: {
validator: $.str
},
password: {
validator: $.str
},
challengeId: {
validator: $.str
},
name: {
validator: $.str
}
}
};
const rpIdHashReal = hash(Buffer.from(config.hostname, 'utf-8'));
export default define(meta, async (ps, user) => {
const profile = await UserProfiles.findOne(user.id).then(ensure);
// Compare password
const same = await bcrypt.compare(ps.password, profile.password!);
if (!same) {
throw new Error('incorrect password');
}
if (!profile.twoFactorEnabled) {
throw new Error('2fa not enabled');
}
const clientData = JSON.parse(ps.clientDataJSON);
if (clientData.type != 'webauthn.create') {
throw new Error('not a creation attestation');
}
if (clientData.origin != config.scheme + '://' + config.host) {
throw new Error('origin mismatch');
}
const clientDataJSONHash = hash(Buffer.from(ps.clientDataJSON, 'utf-8'));
const attestation = await cborDecodeFirst(ps.attestationObject);
const rpIdHash = attestation.authData.slice(0, 32);
if (!rpIdHashReal.equals(rpIdHash)) {
throw new Error('rpIdHash mismatch');
}
const flags = attestation.authData[32];
// tslint:disable-next-line:no-bitwise
if (!(flags & 1)) {
throw new Error('user not present');
}
const authData = Buffer.from(attestation.authData);
const credentialIdLength = authData.readUInt16BE(53);
const credentialId = authData.slice(55, 55 + credentialIdLength);
const publicKeyData = authData.slice(55 + credentialIdLength);
const publicKey: Map<number, any> = await cborDecodeFirst(publicKeyData);
if (publicKey.get(3) != -7) {
throw new Error('alg mismatch');
}
if (!(procedures as any)[attestation.fmt]) {
throw new Error('unsupported fmt');
}
const verificationData = (procedures as any)[attestation.fmt].verify({
attStmt: attestation.attStmt,
authenticatorData: authData,
clientDataHash: clientDataJSONHash,
credentialId,
publicKey,
rpIdHash
});
if (!verificationData.valid) throw new Error('signature invalid');
const attestationChallenge = await AttestationChallenges.findOne({
userId: user.id,
id: ps.challengeId,
registrationChallenge: true,
challenge: hash(clientData.challenge).toString('hex')
});
if (!attestationChallenge) {
throw new Error('non-existent challenge');
}
await AttestationChallenges.delete({
userId: user.id,
id: ps.challengeId
});
// Expired challenge (> 5min old)
if (
new Date().getTime() - attestationChallenge.createdAt.getTime() >=
5 * 60 * 1000
) {
throw new Error('expired challenge');
}
const credentialIdString = credentialId.toString('hex');
await UserSecurityKeys.save({
userId: user.id,
id: credentialIdString,
lastUsed: new Date(),
name: ps.name,
publicKey: verificationData.publicKey.toString('hex')
});
// Publish meUpdated event
publishMainStream(user.id, 'meUpdated', await Users.pack(user.id, user, {
detail: true,
includeSecrets: true
}));
return {
id: credentialIdString,
name: ps.name
};
});

View file

@ -0,0 +1,60 @@
import $ from 'cafy';
import * as bcrypt from 'bcryptjs';
import define from '../../../define';
import { UserProfiles, AttestationChallenges } from '../../../../../models';
import { ensure } from '../../../../../prelude/ensure';
import { promisify } from 'util';
import * as crypto from 'crypto';
import { genId } from '../../../../../misc/gen-id';
import { hash } from '../../../2fa';
const randomBytes = promisify(crypto.randomBytes);
export const meta = {
requireCredential: true,
secure: true,
params: {
password: {
validator: $.str
}
}
};
export default define(meta, async (ps, user) => {
const profile = await UserProfiles.findOne(user.id).then(ensure);
// Compare password
const same = await bcrypt.compare(ps.password, profile.password!);
if (!same) {
throw new Error('incorrect password');
}
if (!profile.twoFactorEnabled) {
throw new Error('2fa not enabled');
}
// 32 byte challenge
const entropy = await randomBytes(32);
const challenge = entropy.toString('base64')
.replace(/=/g, '')
.replace(/\+/g, '-')
.replace(/\//g, '_');
const challengeId = genId();
await AttestationChallenges.save({
userId: user.id,
id: challengeId,
challenge: hash(Buffer.from(challenge, 'utf-8')).toString('hex'),
createdAt: new Date(),
registrationChallenge: true
});
return {
challengeId,
challenge
};
});

Some files were not shown because too many files have changed in this diff Show more