|
import base64 |
|
import sys |
|
from urllib.parse import urlparse, parse_qs, unquote |
|
|
|
from google.protobuf import descriptor_pb2, descriptor_pool, message_factory |
|
|
|
# Build the MigrationPayload protobuf dynamically |
|
fd = descriptor_pb2.FileDescriptorProto() |
|
fd.name = "migration.proto" |
|
|
|
enum = fd.enum_type.add() |
|
enum.name = "Algorithm" |
|
for name, num in [("ALGORITHM_UNSPECIFIED",0),("SHA1",1),("SHA256",2),("SHA512",3),("MD5",4)]: |
|
v = enum.value.add() |
|
v.name = name |
|
v.number = num |
|
|
|
enum = fd.enum_type.add() |
|
enum.name = "DigitCount" |
|
for name, num in [("DIGIT_COUNT_UNSPECIFIED",0),("SIX",1),("EIGHT",2)]: |
|
v = enum.value.add() |
|
v.name = name |
|
v.number = num |
|
|
|
enum = fd.enum_type.add() |
|
enum.name = "OtpType" |
|
for name, num in [("OTP_TYPE_UNSPECIFIED",0),("HOTP",1),("TOTP",2)]: |
|
v = enum.value.add() |
|
v.name = name |
|
v.number = num |
|
|
|
otp = fd.message_type.add() |
|
otp.name = "OtpParameters" |
|
|
|
fields = [ |
|
("secret",1,12,None), |
|
("name",2,9,None), |
|
("issuer",3,9,None), |
|
("algorithm",4,14,"Algorithm"), |
|
("digits",5,14,"DigitCount"), |
|
("type",6,14,"OtpType"), |
|
("counter",7,4,None), |
|
] |
|
|
|
for name, num, typ, typname in fields: |
|
f = otp.field.add() |
|
f.name = name |
|
f.number = num |
|
f.label = 1 |
|
f.type = typ |
|
if typname: |
|
f.type_name = typname |
|
|
|
payload = fd.message_type.add() |
|
payload.name = "MigrationPayload" |
|
|
|
f = payload.field.add() |
|
f.name = "otp_parameters" |
|
f.number = 1 |
|
f.label = 3 |
|
f.type = 11 |
|
f.type_name = "OtpParameters" |
|
|
|
for name, num in [ |
|
("version",2), |
|
("batch_size",3), |
|
("batch_index",4), |
|
("batch_id",5), |
|
]: |
|
f = payload.field.add() |
|
f.name = name |
|
f.number = num |
|
f.label = 1 |
|
f.type = 13 |
|
|
|
pool = descriptor_pool.DescriptorPool() |
|
pool.Add(fd) |
|
|
|
MigrationPayload = message_factory.GetMessageClass( |
|
pool.FindMessageTypeByName("MigrationPayload") |
|
) |
|
|
|
uri = input("Paste otpauth-migration URI:\n").strip() |
|
|
|
qs = parse_qs(urlparse(uri).query) |
|
|
|
if "data" not in qs: |
|
print("No data= parameter found.") |
|
sys.exit(1) |
|
|
|
blob = qs["data"][0] |
|
|
|
padding = "=" * (-len(blob) % 4) |
|
raw = base64.urlsafe_b64decode(blob + padding) |
|
|
|
msg = MigrationPayload() |
|
msg.ParseFromString(raw) |
|
|
|
ALG = { |
|
1: "SHA1", |
|
2: "SHA256", |
|
3: "SHA512", |
|
4: "MD5", |
|
} |
|
|
|
DIGITS = { |
|
1: 6, |
|
2: 8, |
|
} |
|
|
|
TYPE = { |
|
1: "hotp", |
|
2: "totp", |
|
} |
|
|
|
for i, otp in enumerate(msg.otp_parameters, 1): |
|
secret = base64.b32encode(otp.secret).decode().rstrip("=") |
|
|
|
issuer = otp.issuer |
|
account = otp.name |
|
|
|
label = account |
|
if issuer: |
|
label = f"{issuer}:{account}" |
|
|
|
params = [ |
|
f"secret={secret}", |
|
] |
|
|
|
if issuer: |
|
params.append(f"issuer={issuer}") |
|
|
|
if otp.algorithm != 1: |
|
params.append(f"algorithm={ALG.get(otp.algorithm,'SHA1')}") |
|
|
|
if otp.digits == 2: |
|
params.append("digits=8") |
|
|
|
if otp.type == 1: |
|
params.append(f"counter={otp.counter}") |
|
|
|
uri = f"otpauth://{TYPE.get(otp.type,'totp')}/{unquote(label)}?" + "&".join(params) |
|
|
|
print("=" * 70) |
|
print(f"Account #{i}") |
|
print(uri) |