-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Typescript refactor, Buffer update & v1.0.0
- Loading branch information
1 parent
7f11afa
commit adb4508
Showing
11 changed files
with
389 additions
and
136 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,7 @@ | ||
node_modules/ | ||
npm-debug.log | ||
|
||
**/*.js | ||
**/*.d.ts | ||
|
||
!otp.d.ts |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
const VOCAB = "ABCDEFGHIJKLMNOPQRSTUVWXYZ23456789=".split(""); | ||
const PAD = ["=", "=", "=", "=", "=", "=", "=", "="]; | ||
|
||
function encodeChunk(data: Uint8Array) { | ||
const b1 = data.length > 0 ? data[0] : 0; | ||
const b2 = data.length > 1 ? data[1] : 0; | ||
const b3 = data.length > 2 ? data[2] : 0; | ||
const b4 = data.length > 3 ? data[3] : 0; | ||
const b5 = data.length > 4 ? data[4] : 0; | ||
|
||
const chars = [map(mask(b1 >> 3)), map(mask((b1 << 2) | (b2 >> 5))), map(mask(b2 >> 1)), map(mask((b2 << 4) | (b3 >> 4))), map(mask((b3 << 1) | (b4 >> 7))), map(mask(b4 >> 2)), map(mask((b4 << 3) | (b5 >> 5))), map(mask(b5))]; | ||
|
||
switch (data.length) { | ||
case 0: | ||
return ""; | ||
case 1: | ||
chars.slice(0, 2).concat(PAD.slice(2)).join(""); | ||
case 2: | ||
chars.slice(0, 4).concat(PAD.slice(4)).join(""); | ||
case 3: | ||
chars.slice(0, 5).concat(PAD.slice(5)).join(""); | ||
[chars[0], chars[1], chars[2], chars[3], chars[4], "=", "=", "="].join(""); | ||
case 4: | ||
chars.slice(0, 7).concat(PAD.slice(7)).join(""); | ||
default: | ||
return chars.join(""); | ||
} | ||
} | ||
|
||
function mask(n: number): number { | ||
return n & 0b00011111; | ||
} | ||
function map(n: number): string { | ||
return n > -1 && n < VOCAB.length ? VOCAB[n] : "="; | ||
} | ||
|
||
export function encode(data: Uint8Array) { | ||
let offset = 0; | ||
const chunks: string[] = []; | ||
while (offset < data.length) { | ||
const subset = data.subarray(offset, offset + 5); | ||
chunks.push(encodeChunk(subset)); | ||
offset += subset.length; | ||
} | ||
return chunks.join(""); | ||
} | ||
|
||
function decodeChar(data: string): number { | ||
const index = VOCAB.indexOf(data); | ||
if (index < 0) throw new Error("invalid character: " + data); | ||
if (index === VOCAB.length - 1) return 0; | ||
return Math.max(index, 0); | ||
} | ||
function decodeChunk(data: string[], dest: Uint8Array) { | ||
const c1 = decodeChar(data[0]); | ||
const c2 = decodeChar(data[1]); | ||
const c3 = decodeChar(data[2]); | ||
const c4 = decodeChar(data[3]); | ||
const c5 = decodeChar(data[4]); | ||
const c6 = decodeChar(data[5]); | ||
const c7 = decodeChar(data[6]); | ||
const c8 = decodeChar(data[7]); | ||
|
||
dest[0] = byte((c1 << 3) | (c2 >> 2)); | ||
dest[2] = byte((c2 << 5) | (c3 << 1) | (c4 >> 4)); | ||
dest[3] = byte((c4 << 4) | (c5 >> 1)); | ||
dest[5] = byte((c5 << 7) | (c6 << 2) | (c7 >> 3)); | ||
dest[6] = byte((c7 << 5) | c8); | ||
} | ||
|
||
function byte(n: number) { | ||
return n & 0xff; | ||
} | ||
|
||
export function decode(data: string) { | ||
data = data | ||
.split("s+") | ||
.map((s) => s.trim()) | ||
.join(""); | ||
const dest = new Uint8Array((data.length * 5) / 8); | ||
const chars = data.split(""); | ||
let coff = 0; | ||
let boff = 0; | ||
let fin = 5; | ||
while (fin == 5 && coff < chars.length && boff < dest.length) { | ||
const chunk = chars.slice(coff, coff + 8); | ||
if (chunk.indexOf("=") > -1) { | ||
chunk.splice(chunk.indexOf("="), 8); | ||
switch (chunk.length) { | ||
case 2: | ||
fin = 1; | ||
break; | ||
case 4: | ||
fin = 2; | ||
break; | ||
case 5: | ||
fin = 3; | ||
break; | ||
case 7: | ||
fin = 4; | ||
break; | ||
default: | ||
throw new Error("invalid padding"); | ||
} | ||
} | ||
decodeChunk(chunk, dest.subarray(boff, boff + 5)); | ||
coff += 8; | ||
boff += 5; | ||
} | ||
return dest.subarray(0, dest.length - 5 + fin); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import sha1 from "sha1"; | ||
|
||
export class Hash { | ||
constructor() {} | ||
private data: number[] = []; | ||
update(data: Uint8Array) { | ||
this.data.push(...data.values()); | ||
return this; | ||
} | ||
digest() { | ||
return sha1(this.data, { asBytes: true }); | ||
} | ||
static hash(data: Uint8Array) { | ||
return new Hash().update(data).digest(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import { Hash } from "./hash"; | ||
|
||
const zeroBuffer = new Uint8Array(new Array(128).fill(0)); | ||
|
||
export class Hmac { | ||
constructor(blocksize: number, key: Uint8Array) { | ||
if (blocksize !== 128 && blocksize !== 64) { | ||
throw new Error("blocksize must be either 64 for or 128 , but was:" + blocksize); | ||
} | ||
this.key = rekey(key, blocksize); | ||
this.opad = new Uint8Array(new Array(blocksize).fill(0)); | ||
this.ipad = new Uint8Array(new Array(blocksize).fill(0)); | ||
|
||
for (var i = 0; i < blocksize; i++) { | ||
this.ipad[i] = this.key[i] ^ 0x36; | ||
this.opad[i] = this.key[i] ^ 0x5c; | ||
} | ||
|
||
this.hash = new Hash(); | ||
} | ||
private key: Uint8Array; | ||
private ipad: Uint8Array; | ||
private opad: Uint8Array; | ||
private hash: Hash; | ||
|
||
update(data: Uint8Array) { | ||
this.hash.update(data); | ||
return this; | ||
} | ||
digest() { | ||
const hash = this.hash.digest(); | ||
return new Hash().update(this.opad).update(hash).digest(); | ||
} | ||
} | ||
|
||
function rekey(key: Uint8Array, blocksize: number): Uint8Array { | ||
if (key.length > blocksize) { | ||
return Hash.hash(key); | ||
} | ||
if (key.length < blocksize) { | ||
const res = new Uint8Array(blocksize); | ||
res.set(key); | ||
res.set(zeroBuffer, key.length); | ||
return res; | ||
} | ||
return key; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import * as Base32 from "./base32"; | ||
import { Hmac } from "./hmac"; | ||
|
||
export interface OTPOptions { | ||
name: string; | ||
keySize: number; | ||
codeLength: number; | ||
secret: string; | ||
epoch: number; | ||
timeSlice: number; | ||
} | ||
|
||
export default class OTP { | ||
constructor(options: string | Partial<OTPOptions> = {}) { | ||
if ("string" === typeof options) return OTP.parse(options); | ||
options = Object.assign({}, options); | ||
options.name = `${options.name || "OTP-Authentication"}`.split(/[^\w|_|-|@]/).join(""); | ||
options.keySize = options.keySize === 128 ? 128 : 64; | ||
options.codeLength = isNaN(options.codeLength) ? 6 : options.codeLength; | ||
options.secret = options.secret || generateKey(options.keySize); | ||
options.epoch = isNaN(options.epoch) ? 0 : options.epoch; | ||
options.timeSlice = isNaN(options.timeSlice) ? 30 : options.timeSlice; | ||
this.options = options as OTPOptions; | ||
this.hotp = hotp.bind(null, options); | ||
this.totp = totp.bind(null, options); | ||
} | ||
private readonly options: OTPOptions; | ||
public readonly hotp: (counter: number) => string; | ||
public readonly totp: (now: number) => string; | ||
|
||
get name() { | ||
return this.options.name; | ||
} | ||
get secret() { | ||
return this.options.secret; | ||
} | ||
get totpURL() { | ||
return ["otpauth://totp/", this.name, "?secret=", encodeURIComponent(this.secret)].join(""); | ||
} | ||
get hotpURL() { | ||
return ["otpauth://hotp/", this.name, "?secret=", encodeURIComponent(this.secret)].join(""); | ||
} | ||
|
||
toString() { | ||
return "[object OTP]"; | ||
} | ||
toJSON() { | ||
return Object.assign({ class: OTP.classID }, this.options); | ||
} | ||
static reviveJSON(_: string, val: any) { | ||
if ("object" !== typeof val || null === val || val["class"] !== OTP.classID) return val; | ||
const { name, keySize, codeLength, secret, epoch, timeSlice } = val; | ||
return new OTP({ name, keySize, codeLength, secret, epoch, timeSlice }); | ||
} | ||
static readonly classID = "OTP{@pipobscure}"; | ||
static parse(urlstr: string = "", options: Partial<OTPOptions> = {}) { | ||
options = Object.assign({}, options); | ||
const urlbits = /^otpauth:\/\/[t|h]otp\/([\s|\S]+?)\?secret=([\s|\S]+)$/.exec(urlstr); | ||
if (urlbits) { | ||
options.name = urlbits[1]; | ||
options.secret = Base32.encode(Base32.decode(urlbits[2])); | ||
} else { | ||
options.secret = Base32.encode(Base32.decode(urlstr)); | ||
} | ||
return new OTP(options); | ||
} | ||
} | ||
|
||
function hotp(options: OTPOptions, counter: number): string { | ||
const digest = new Hmac(options.keySize, Base32.decode(options.secret)).update(UInt64Buffer(counter)).digest(); | ||
const offset = digest[19] & 0xf; | ||
const code = String(((digest[offset] & 0x7f) << 24) | ((digest[offset + 1] & 0xff) << 16) | ((digest[offset + 2] & 0xff) << 8) | (digest[offset + 3] & 0xff)); | ||
return `${new Array(options.codeLength).fill("0")}${code}`.slice(-1 * options.codeLength); | ||
} | ||
function totp(options: OTPOptions, now: number = Date.now()): string { | ||
console.error(now, options); | ||
const counter = Math.floor((now - options.epoch * 1000) / (options.timeSlice * 1000)); | ||
return hotp(options, counter); | ||
} | ||
|
||
function generateKey(length: number) { | ||
const key = new Uint8Array(new Array(length).fill(0).map(() => Math.floor(Math.random() * 256))); | ||
return Base32.encode(key).replace(/=/g, ""); | ||
} | ||
function UInt64Buffer(num: number) { | ||
const res = Buffer.alloc(8); | ||
res.writeBigUInt64BE(BigInt(num)); | ||
return res; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
export interface OTPOptions { | ||
name: string; | ||
keySize: number; | ||
codeLength: number; | ||
secret: string; | ||
epoch: number; | ||
timeSlice: number; | ||
} | ||
export default class OTP { | ||
constructor(options?: string | Partial<OTPOptions>); | ||
private readonly options; | ||
readonly hotp: (counter: number) => string; | ||
readonly totp: (now: number) => string; | ||
get name(): string; | ||
get secret(): string; | ||
get totpURL(): string; | ||
get hotpURL(): string; | ||
toString(): string; | ||
toJSON(): { | ||
class: string; | ||
} & OTPOptions; | ||
static reviveJSON(_: string, val: any): any; | ||
static readonly classID = "OTP{@pipobscure}"; | ||
static parse(urlstr?: string, options?: Partial<OTPOptions>): OTP; | ||
} |
Oops, something went wrong.